From 8a1059e925a1ad0ace66c8822678e5f1c1fc1e5b Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 12:23:43 +0200 Subject: [PATCH 01/15] SVN: UX Improvements - Add option to open file with UI instead of only without - Clarify that UI will be kept when reloading a file - Reloading a file now prompts if current file is dirty (only from right-click menu) - Fix file list and SVN log not being updated when opening a new file - Fix file list getting permanently grayed out when trashing a file - Move outdated file warning from 3DView header to the top-bar Minor code tweaks: - Move force_good_active_index inside of update_file_filter - Rename update_file_filter to refresh_ui_lists - Remove file open operator. I figured out how to use the built-in one --- .../blender_svn/operators/simple_commands.py | 10 ++-- .../addons/blender_svn/repository.py | 50 ++++++++----------- .../addons/blender_svn/threaded/svn_status.py | 3 +- .../addons/blender_svn/ui/ui_context_menus.py | 33 +++++------- .../blender_svn/ui/ui_outdated_warning.py | 4 +- 5 files changed, 42 insertions(+), 58 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 08952342..0ac1b968 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -20,7 +20,7 @@ class SVN_Operator: @staticmethod def update_file_list(context): repo = context.scene.svn.get_repo(context) - repo.update_file_filter(context) + repo.refresh_ui_lists(context) def execute_svn_command(self, context, command: List[str], use_cred=False) -> str: # Since a status update might already be being requested when an SVN operator is run, @@ -51,9 +51,8 @@ class SVN_Operator_Single_File(SVN_Operator): ret = self._execute(context) file = self.get_file(context) - if file: - Processes.start('Status') - redraw_viewport() + Processes.start('Status') + redraw_viewport() self.update_file_list(context) return ret @@ -107,7 +106,7 @@ class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator): return current_blend and current_blend.svn_path == self.file_rel_path reload_file: BoolProperty( - name="Reload File", + name="Reload File (Keep UI)", description="Reload the file after the operation is completed. The UI layout will be preserved", default=False, ) @@ -305,6 +304,7 @@ class SVN_OT_trash_file(SVN_Operator_Single_File, Warning_Operator, Operator): bl_options = {'INTERNAL'} file_rel_path: StringProperty() + missing_file_allowed = False def get_warning_text(self, context): return "Are you sure you want to move this file to the recycle bin?\n " + self.file_rel_path diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index b920ca41..36253d93 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -444,15 +444,15 @@ class SVN_repository(PropertyGroup): """When user clicks on a different file, the latest log entry of that file should become the active log entry.""" - latest_idx = self.get_latest_revision_of_file( + latest_rev = self.get_latest_revision_of_file( self.active_file.svn_path) # SVN Revisions are not 0-indexed, so we need to subtract 1. - self.log_active_index = latest_idx-1 + self.log_active_index = latest_rev-1 space = context.space_data if space and space.type == 'FILE_BROWSER': # Set the active file in the file browser to whatever was selected in the SVN Files panel. - self.log_active_index_filebrowser = latest_idx-1 + self.log_active_index_filebrowser = latest_rev-1 space.params.directory = self.active_file.absolute_path.parent.as_posix().encode() space.params.filename = self.active_file.name.encode() @@ -462,7 +462,7 @@ class SVN_repository(PropertyGroup): relative_path=self.active_file.name) Processes.start('Activate File') - # Filter out log entries that did not affect the selected file. + # Set the filter flag of the log entries based on whether they affect the active file or not. self.log.foreach_set( 'affects_active_file', [log_entry.changes_file(self.active_file) @@ -513,28 +513,10 @@ class SVN_repository(PropertyGroup): return self.get_file_by_absolute_path(bpy.data.filepath) ### File List UIList filter properties ### - # Filtering properties are normally stored on the UIList, - # but then they cannot be accessed from anywhere else, - # since template_list() does not return the UIList instance. - # We need to be able to access them outside of drawing code, to be able to - # ensure that a filtered out entry can never be the active one. - - def force_good_active_index(self, context) -> bool: - """ - We want to avoid having the active file entry be invisible due to filtering. - If the active element is being filtered out, set the active element to - something that is visible. - """ - if len(self.external_files) == 0: - return - if not self.active_file.show_in_filelist: - for i, file in enumerate(self.external_files): - if file.show_in_filelist: - self.external_files_active_index = i - return - - def update_file_filter(self, context): - """Should run when any of the SVN file list search filters are changed.""" + def refresh_ui_lists(self, context): + """Refresh the file UI list based on filter settings. + Also triggers a refresh of the SVN UIList, through the update callback of + external_files_active_index.""" UI_LIST = bpy.types.UI_UL_list if self.file_search_filter: @@ -555,12 +537,24 @@ class SVN_repository(PropertyGroup): file.show_in_filelist = not file.has_default_status - self.force_good_active_index(context) + if len(self.external_files) == 0: + return + if self.active_file.show_in_filelist: + # Trigger the update callback, because that refreshes the SVN Log UI. + self.external_files_active_index = self.external_files_active_index + return + + # Make sure the active file isn't now being filtered out. + # If it is, change the active file to the first visible one. + for i, file in enumerate(self.external_files): + if file.show_in_filelist: + self.external_files_active_index = i + return file_search_filter: StringProperty( name="Search Filter", description="Only show entries that contain this string", - update=update_file_filter + update=refresh_ui_lists ) diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 06f85ac8..1de9852d 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -277,8 +277,7 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]): if file_entry.svn_path not in svn_paths: repo.remove_file_entry(file_entry) - repo.update_file_filter(context) - repo.force_good_active_index(context) + repo.refresh_ui_lists(context) def get_repo_file_statuses(svn_status_str: str) -> Dict[str, Tuple[str, str, int]]: diff --git a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py index 8082620b..59d9f12a 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py +++ b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py @@ -3,26 +3,10 @@ import bpy from bpy.types import Context, UIList, Operator -from bpy.props import StringProperty +from bpy.props import StringProperty, BoolProperty from pathlib import Path -class SVN_OT_open_blend_file(Operator): - # This is needed because drawing a button for wm.open_mainfile in the UI - # directly simply does not work; Blender just opens a full-screen filebrowser, - # instead of opening the .blend file. Probably a bug. - bl_idname = "svn.open_blend_file" - bl_label = "Open Blend File" - bl_description = "Open Blend File" - bl_options = {'INTERNAL'} - - filepath: StringProperty() - - def execute(self, context): - bpy.ops.wm.open_mainfile(filepath=self.filepath, load_ui=False) - return {'FINISHED'} - - def check_context_match(context: Context, uilayout_type: str, bl_idname: str) -> bool: """For example, when right-clicking on a UIList, the uilayout_type will be `ui_list` and the bl_idname is that of the UIList being right-clicked. @@ -39,8 +23,17 @@ def svn_file_list_context_menu(self: UIList, context: Context) -> None: layout.separator() active_file = context.scene.svn.get_repo(context).active_file if active_file.name.endswith("blend"): - layout.operator("svn.open_blend_file", - text=f"Open {active_file.name}").filepath = active_file.absolute_path + op = layout.operator("wm.open_mainfile", + text=f"Open {active_file.name}") + op.filepath = active_file.absolute_path + op.display_file_selector = False + op.load_ui = True + op = layout.operator("wm.open_mainfile", + text=f"Open {active_file.name} (Keep UI)") + op.filepath = active_file.absolute_path + op.display_file_selector = False + op.load_ui = False + else: layout.operator("wm.path_open", text=f"Open {active_file.name}").filepath = str(Path(active_file.absolute_path)) @@ -73,5 +66,3 @@ def unregister(): bpy.types.UI_MT_list_item_context_menu.remove(svn_file_list_context_menu) bpy.types.UI_MT_list_item_context_menu.remove(svn_log_list_context_menu) - -registry = [SVN_OT_open_blend_file] diff --git a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py index 5b4a6cce..75d1ef26 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py +++ b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py @@ -34,8 +34,8 @@ def draw_outdated_file_warning(self, context): def register(): - bpy.types.VIEW3D_HT_header.prepend(draw_outdated_file_warning) + bpy.types.TOPBAR_MT_editor_menus.append(draw_outdated_file_warning) def unregister(): - bpy.types.VIEW3D_HT_header.remove(draw_outdated_file_warning) + bpy.types.TOPBAR_MT_editor_menus.remove(draw_outdated_file_warning) -- 2.30.2 From 4ed941516563febcf3bb92e9dd67f7e9a2469e60 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 13:43:48 +0200 Subject: [PATCH 02/15] - Outdated file warning now clickable - Saving the file won't re-authenticate - Loading a file from another repo won't re-authenticate --- .../blender_svn/operators/simple_commands.py | 72 +++++++++++++------ .../threaded/background_process.py | 7 ++ .../addons/blender_svn/threaded/svn_status.py | 26 +++++-- .../addons/blender_svn/ui/ui_file_list.py | 13 +--- .../blender_svn/ui/ui_outdated_warning.py | 5 +- 5 files changed, 78 insertions(+), 45 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 0ac1b968..89021290 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -177,7 +177,7 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator): missing_file_allowed = True - revision: IntProperty() + revision: IntProperty(default=0) def invoke(self, context, event): file_entry = context.scene.svn.get_repo( @@ -198,18 +198,24 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator): "Cancelled: You have local modifications to this file. You must revert or commit it first!") return {'CANCELLED'} - self.execute_svn_command( - context, - ["svn", "up", f"-r{self.revision}", - f"{self.file_rel_path}", "--accept", "postpone"], - use_cred=True - ) + self.svn_download_file_revision(context, self.file_rel_path, self.revision) self.report({'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}") return {"FINISHED"} + def svn_download_file_revision(self, context, svn_file_path: str, revision=0): + commands = ["svn", "up", f"{self.file_rel_path}", "--accept", "postpone"] + if self.revision > 0: + commands.insert(2, f"-r{self.revision}") + + self.execute_svn_command( + context, + commands, + use_cred=True + ) + def set_predicted_file_status(self, repo, file_entry: "SVN_file"): file_entry['revision'] = self.revision latest_rev = repo.get_latest_revision_of_file(self.file_rel_path) @@ -229,13 +235,14 @@ class SVN_OT_restore_file(May_Modifiy_Current_Blend, Operator): missing_file_allowed = True - def _execute(self, context: Context) -> Set[str]: + def svn_revert(self, context, svn_file_path): self.execute_svn_command( context, - ["svn", "revert", f"{self.file_rel_path}"] + ["svn", "revert", f"{svn_file_path}"] ) - f = self.get_file(context) + def _execute(self, context: Context) -> Set[str]: + self.svn_revert(context, self.file_rel_path) return {"FINISHED"} def set_predicted_file_status(self, repo, file_entry: "SVN_file"): @@ -250,15 +257,35 @@ class SVN_OT_revert_file(SVN_OT_restore_file): missing_file_allowed = False - def _execute(self, context: Context) -> Set[str]: - super()._execute(context) - - return {"FINISHED"} - def get_warning_text(self, context) -> str: return "You will irreversibly and permanently lose the changes you've made to this file:\n " + self.file_rel_path +class SVN_OT_revert_and_update(SVN_OT_download_file_revision, SVN_OT_revert_file): + """Convenience operator for the "This file is outdated" warning message. Normally, these two operations should be done separately!""" + bl_idname = "svn.revert_and_update_file" + bl_label = "Revert And Update File" + bl_description = "The currently opened .blend file has a newer version available on the remote repository. Click to PERMANENTLY DISCARD local changes to this file and update it to the latest revision. Cannot be undone" + bl_options = {'INTERNAL'} + + missing_file_allowed = False + + def invoke(self, context, event): + return super(May_Modifiy_Current_Blend, self).invoke(context, event) + + def get_warning_text(self, context) -> str: + if self.get_file(context).status != 'normal': + return "You will irreversibly and permanently lose the changes you've made to this file:\n " + self.file_rel_path + else: + return "File will be updated to latest revision." + + def _execute(self, context: Context) -> Set[str]: + self.svn_revert(context, self.file_rel_path) + self.svn_download_file_revision(context, self.file_rel_path, self.revision) + + return {"FINISHED"} + + class SVN_OT_add_file(SVN_Operator_Single_File, Operator): bl_idname = "svn.add_file" bl_label = "Add File" @@ -411,15 +438,13 @@ class SVN_OT_cleanup(SVN_Operator, Operator): self.execute_svn_command(context, ["svn", "cleanup"]) repo.reload_svn_log(context) - Processes.kill('Status') - Processes.kill('Log') Processes.kill('Commit') Processes.kill('Update') Processes.kill('Authenticate') Processes.kill('Activate File') - Processes.start('Status') - Processes.start('Log') + Processes.restart('Status') + Processes.restart('Log') self.report({'INFO'}, "SVN Cleanup complete.") @@ -428,13 +453,14 @@ class SVN_OT_cleanup(SVN_Operator, Operator): registry = [ SVN_OT_update_single, - SVN_OT_download_file_revision, - SVN_OT_revert_file, + SVN_OT_revert_and_update, SVN_OT_restore_file, - SVN_OT_unadd_file, + SVN_OT_revert_file, + SVN_OT_download_file_revision, SVN_OT_add_file, + SVN_OT_unadd_file, SVN_OT_trash_file, SVN_OT_remove_file, - SVN_OT_cleanup, SVN_OT_resolve_conflict, + SVN_OT_cleanup, ] diff --git a/scripts-blender/addons/blender_svn/threaded/background_process.py b/scripts-blender/addons/blender_svn/threaded/background_process.py index 983a7a06..32c4a6b1 100644 --- a/scripts-blender/addons/blender_svn/threaded/background_process.py +++ b/scripts-blender/addons/blender_svn/threaded/background_process.py @@ -214,6 +214,7 @@ class ProcessManager: def processes(self): # I tried to implement this thing as a Singleton that inherits from the `dict` class, # I tried having the `processes` dict on the class level, + # I tried having it on the instance level, # and it just refuses to work properly. I add an instance to the dictionary, # I print it, I can see that it's there, I make sure it absolutely doesn't get removed, # but when I try to access it from anywhere, it's just empty. My mind is boggled. @@ -260,6 +261,12 @@ class ProcessManager: if process: process.stop() del self.processes[proc_name] + + def restart(self, proc_name: str): + """Destroy a process, then start it again. + Useful to skip the repeat_delay timer of infinite processes like Status or Log.""" + self.kill(proc_name) + self.start(proc_name) # I named this variable with title-case, to indicate that it's a Singleton. diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 1de9852d..6406222a 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -55,8 +55,9 @@ class SVN_OT_explain_status(Operator): @bpy.app.handlers.persistent -def init_svn_of_current_file(_scene=None): - """When opening or saving a .blend file: +def ensure_svn_of_current_file(_scene=None): + """When opening or saving a .blend file, it's possible that the new .blend + is part of an SVN repository. If this is the case, do the following: - Initialize SVN Scene info - Initialize Repository - Try to authenticate @@ -68,6 +69,17 @@ def init_svn_of_current_file(_scene=None): prefs = get_addon_prefs(context) prefs.sync_repo_info_file() + repo = prefs.active_repo + if repo and repo.is_cred_entered and repo.authenticated: + status = Processes.get('Status') + if status and time.time() - status.timestamp_last_update < status.repeat_delay + 5: + # If the SVN Status background process has recently processed data, + # there is no need to re-initialize everything. + # This happens when a file that was already saved is saved again, + # or a file from the same repository as the previous one is loaded. + Processes.restart('Status') + return + for repo in prefs.repositories: # This would ideally only run when opening Blender for the first # time, but there is no app handler for that, sadly. @@ -338,22 +350,22 @@ def mark_current_file_as_modified(_dummy1=None, _dummy2=None): def delayed_init_svn(delay=1): - bpy.app.timers.register(init_svn_of_current_file, first_interval=delay) + bpy.app.timers.register(ensure_svn_of_current_file, first_interval=delay) def register(): - bpy.app.handlers.load_post.append(init_svn_of_current_file) + bpy.app.handlers.load_post.append(ensure_svn_of_current_file) - bpy.app.handlers.save_post.append(init_svn_of_current_file) + bpy.app.handlers.save_post.append(ensure_svn_of_current_file) bpy.app.handlers.save_post.append(mark_current_file_as_modified) delayed_init_svn() def unregister(): - bpy.app.handlers.load_post.remove(init_svn_of_current_file) + bpy.app.handlers.load_post.remove(ensure_svn_of_current_file) - bpy.app.handlers.save_post.remove(init_svn_of_current_file) + bpy.app.handlers.save_post.remove(ensure_svn_of_current_file) bpy.app.handlers.save_post.remove(mark_current_file_as_modified) diff --git a/scripts-blender/addons/blender_svn/ui/ui_file_list.py b/scripts-blender/addons/blender_svn/ui/ui_file_list.py index b6c460cb..bc85d221 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_file_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_file_list.py @@ -114,6 +114,7 @@ class SVN_UL_file_list(UIList): explainer.status = status explainer.file_rel_path = file_entry.svn_path + @classmethod def cls_filter_items(cls, context, data, propname): """By moving all of this logic to a classmethod (and all the filter @@ -188,13 +189,6 @@ def draw_repo_file_list(context, layout, repo): return main_col = layout.column() - main_col.enabled = False - status_proc = Processes.get('Status') - time_since_last_update = 1000 - if status_proc: - time_since_last_update = time.time() - status_proc.timestamp_last_update - if time_since_last_update < 30: - main_col.enabled = True main_row = main_col.row() split = main_row.split(factor=0.6) filepath_row = split.row() @@ -207,11 +201,6 @@ def draw_repo_file_list(context, layout, repo): ops_row.alignment = 'RIGHT' ops_row.label(text="Operations") - timer_row = main_row.row() - timer_row.alignment = 'RIGHT' - timer_row.operator("svn.custom_tooltip", icon='BLANK1', text="", - emboss=False).tooltip = "Time since last file status update: " + str(time_since_last_update) + 's' - row = main_col.row() row.template_list( "SVN_UL_file_list", diff --git a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py index 75d1ef26..8f6afb3d 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py +++ b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py @@ -28,9 +28,8 @@ def draw_outdated_file_warning(self, context): row.operator('svn.resolve_conflict', text="SVN: This .blend file is conflicted.", icon='ERROR') elif current_file.repos_status != 'none': - warning = row.operator( - 'svn.custom_tooltip', text="SVN: This .blend file is outdated.", icon='ERROR') - warning.tooltip = "The currently opened .blend file has a newer version available on the remote repository. This means any changes in this file will result in a conflict, and potential loss of data. See the SVN panel for info" + op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file is outdated.", icon='ERROR') + op.file_rel_path = repo.current_blend_file.svn_path def register(): -- 2.30.2 From cad91a94a0e401d6342bff0311ff9353b8468c70 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 15:23:41 +0200 Subject: [PATCH 03/15] Add support for when SVN isn't on the system --- scripts-blender/addons/blender_svn/prefs.py | 6 +++++ scripts-blender/addons/blender_svn/props.py | 13 ++++++++--- .../threaded/execute_subprocess.py | 8 ++++++- .../addons/blender_svn/threaded/svn_status.py | 11 ++++++++-- .../addons/blender_svn/ui/ui_file_list.py | 6 +++-- .../addons/blender_svn/ui/ui_log.py | 3 +++ .../addons/blender_svn/ui/ui_prefs.py | 22 +++++++++++++++++++ 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index 955ee11a..e2c94296 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -19,6 +19,12 @@ from .threaded.background_process import Processes class SVN_addon_preferences(AddonPreferences): bl_idname = __package__ + is_svn_installed: BoolProperty( + name="Is SVN Installed", + description="Whether the `svn` command works at all in the user's command line. If not, user needs to install SVN", + default=False + ) + repositories: CollectionProperty(type=SVN_repository) def init_repo(self, context, repo_path: Path or str): diff --git a/scripts-blender/addons/blender_svn/props.py b/scripts-blender/addons/blender_svn/props.py index 39a383ca..e7b8faf4 100644 --- a/scripts-blender/addons/blender_svn/props.py +++ b/scripts-blender/addons/blender_svn/props.py @@ -27,12 +27,14 @@ class SVN_scene_properties(PropertyGroup): description="Absolute directory path of the SVN repository's root in the file system", ) - def get_repo(self, context): + def get_repo(self, context) -> Optional['SVN_repository']: """Return the current repository. Depending on preferences, this is either the repo the current .blend file is in, or whatever repo is selected in the preferences UI. """ prefs = get_addon_prefs(context) + if not prefs.is_svn_installed: + return if prefs.active_repo_mode == 'CURRENT_BLEND': return self.get_scene_repo(context) @@ -40,15 +42,20 @@ class SVN_scene_properties(PropertyGroup): return prefs.active_repo def get_scene_repo(self, context) -> Optional['SVN_repository']: + """Return the repository of the current file, even if the add-on is + configured to another repository. + """ + prefs = get_addon_prefs(context) + if not prefs.is_svn_installed: + return + if not self.svn_url or not self.svn_directory: return - prefs = get_addon_prefs(context) for repo in prefs.repositories: if (repo.url == self.svn_url) and (Path(repo.directory) == Path(self.svn_directory)): return repo - registry = [ SVN_scene_properties, ] diff --git a/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py b/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py index 7e395c9b..6816fb28 100644 --- a/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py +++ b/scripts-blender/addons/blender_svn/threaded/execute_subprocess.py @@ -4,7 +4,6 @@ import subprocess from typing import List - def get_credential_commands(context) -> List[str]: repo = context.scene.svn.get_repo(context) assert (repo.is_cred_entered), "No username or password entered for this repository. The UI shouldn't have allowed you to get into a state where you can press an SVN operation button without having your credentials entered, so this is a bug!" @@ -29,6 +28,9 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri SVN root. """ repo = context.scene.svn.get_repo(context) + if "svn" not in command: + command.insert(0, "svn") + if use_cred: command += get_credential_commands(context) @@ -46,3 +48,7 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri print(f"Command returned error: {command}") print(err_msg) raise error + +def check_svn_installed(): + code, message = subprocess.getstatusoutput('svn') + return code != 127 \ No newline at end of file diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 6406222a..7837d33b 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -4,7 +4,7 @@ from ..svn_info import get_svn_info from ..util import get_addon_prefs from .. import constants -from .execute_subprocess import execute_svn_command +from .execute_subprocess import execute_svn_command, check_svn_installed from .background_process import BackgroundProcess, Processes from bpy.types import Operator from bpy.props import StringProperty @@ -64,11 +64,18 @@ def ensure_svn_of_current_file(_scene=None): """ context = bpy.context + prefs = get_addon_prefs(context) + prefs.is_svn_installed = check_svn_installed() + if not prefs.is_svn_installed: + return + scene_svn = context.scene.svn - prefs = get_addon_prefs(context) prefs.sync_repo_info_file() + if prefs.active_repo_idx == -1 and len(prefs.repositories) > 0: + prefs.active_repo_idx = 0 + repo = prefs.active_repo if repo and repo.is_cred_entered and repo.authenticated: status = Processes.get('Status') diff --git a/scripts-blender/addons/blender_svn/ui/ui_file_list.py b/scripts-blender/addons/blender_svn/ui/ui_file_list.py index bc85d221..23a47326 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_file_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_file_list.py @@ -149,8 +149,10 @@ class SVN_UL_file_list(UIList): row.prop(self, 'show_file_paths', text="", toggle=True, icon="FILE_FOLDER") - row.prop(context.scene.svn.get_repo(context), - 'file_search_filter', text="") + + repo = context.scene.svn.get_repo(context) + if repo: + row.prop(repo, 'file_search_filter', text="") def draw_process_info(context, layout): diff --git a/scripts-blender/addons/blender_svn/ui/ui_log.py b/scripts-blender/addons/blender_svn/ui/ui_log.py index c6d8d64e..a09276a6 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_log.py +++ b/scripts-blender/addons/blender_svn/ui/ui_log.py @@ -90,6 +90,9 @@ class SVN_UL_log(UIList): def is_log_useful(context): repo = context.scene.svn.get_repo(context) + if not repo: + return False + if len(repo.log) == 0 or len(repo.external_files) == 0: return False active_file = repo.active_file diff --git a/scripts-blender/addons/blender_svn/ui/ui_prefs.py b/scripts-blender/addons/blender_svn/ui/ui_prefs.py index bee70ed8..674ca8aa 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_prefs.py +++ b/scripts-blender/addons/blender_svn/ui/ui_prefs.py @@ -99,12 +99,34 @@ class SVN_MT_add_repo(Menu): def draw_prefs(self, context): + if not self.is_svn_installed: + draw_prefs_no_svn(self, context) + return + if self.checkout_mode: draw_prefs_checkout(self, context) else: draw_prefs_repos(self, context) +def draw_prefs_no_svn(self, context): + terminal, url = "terminal", "https://subversion.apache.org/packages.html" + system = platform.system() + if system == "Windows": + terminal = "command line (cmd.exe)" + url = "https://subversion.apache.org/packages.html#windows" + elif system == "Darwin": + terminal = "Mac terminal" + url = "https://subversion.apache.org/packages.html#osx" + + layout = self.layout + col = layout.column() + col.alert=True + col.label(text="Please ensure that Subversion (aka. SVN) is installed on your system.") + col.label(text=f"Typing `svn` into the {terminal} should yield a result.") + layout.operator("wm.url_open", icon='URL', text='Open Subversion Distribution Page').url=url + + def draw_prefs_checkout(self, context): def get_terminal_howto(): msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console." -- 2.30.2 From cbdb3e14a33cd73181bc664c91f8746273af8844 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 17:11:49 +0200 Subject: [PATCH 04/15] Remove active_repo_mode selector This greatly simplifies a lot of both UX and code, however, we seem to lose the ability to not flicker the UI on file save/load... Would like to bring that back. --- .../blender_svn/operators/simple_commands.py | 1 + .../blender_svn/operators/svn_checkout.py | 1 - scripts-blender/addons/blender_svn/prefs.py | 50 ++++---------- scripts-blender/addons/blender_svn/props.py | 27 +------- .../addons/blender_svn/repository.py | 4 +- .../threaded/background_process.py | 16 ++--- .../blender_svn/threaded/redraw_viewport.py | 1 + .../addons/blender_svn/threaded/svn_status.py | 67 ++++++++----------- .../addons/blender_svn/ui/ui_prefs.py | 13 ---- .../addons/blender_svn/ui/ui_sidebar.py | 7 +- 10 files changed, 56 insertions(+), 131 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 89021290..1f473307 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -445,6 +445,7 @@ class SVN_OT_cleanup(SVN_Operator, Operator): Processes.restart('Status') Processes.restart('Log') + Processes.restart('Redraw Viewport') self.report({'INFO'}, "SVN Cleanup complete.") diff --git a/scripts-blender/addons/blender_svn/operators/svn_checkout.py b/scripts-blender/addons/blender_svn/operators/svn_checkout.py index 03be4919..260251bb 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_checkout.py +++ b/scripts-blender/addons/blender_svn/operators/svn_checkout.py @@ -28,7 +28,6 @@ class SVN_OT_checkout_initiate(Operator): def execute(self, context): prefs = get_addon_prefs(context) - prefs.active_repo_mode = 'SELECTED_REPO' if self.create: prefs.repositories.add() prefs.active_repo_idx = len(prefs.repositories)-1 diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index e2c94296..f8fc5cf7 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -46,39 +46,29 @@ class SVN_addon_preferences(AddonPreferences): repo = self.repositories.add() repo.initialize(root_dir, base_url) + self.active_repo_idx = len(self.repositories)-1 return repo def update_active_repo_idx(self, context): - if self.idx_updating or len(self.repositories) == 0: + if len(self.repositories) == 0: return - self.idx_updating = True active_repo = self.active_repo - if self.active_repo_mode == 'CURRENT_BLEND': - scene_svn = context.scene.svn - scene_svn_idx = self.repositories.find(scene_svn.svn_directory) - if scene_svn_idx == -1: - self.idx_updating = False - return - self.active_repo_idx = scene_svn_idx - self.idx_updating = False - return + # Authenticate when switching repos. if ( active_repo and - not active_repo.authenticated and not active_repo.auth_failed and active_repo.is_cred_entered ): - active_repo.authenticate(context) + Processes.start('Redraw Viewport') + if active_repo.authenticated: + Processes.restart('Status') + else: + active_repo.authenticate(context) + else: + Processes.kill('Status') - self.idx_updating = False - - def update_active_repo_mode(self, context): - if self.active_repo_mode == 'CURRENT_BLEND': - scene_svn = context.scene.svn - scene_svn_idx = self.repositories.find(scene_svn.svn_directory) - self.active_repo_idx = scene_svn_idx checkout_mode: BoolProperty( name="Checkout In Progress", @@ -86,30 +76,16 @@ class SVN_addon_preferences(AddonPreferences): default=False ) - active_repo_mode: EnumProperty( - name="Choose Repository", - description="Whether the add-on should communicate with the repository of the currently opened .blend file, or the repository selected in the list below", - items=[ - ('CURRENT_BLEND', "Current Blend", "Check if the current .blend file is in an SVN repository, and communicate with that if that is the case. The file list will display only the files of the repository of the current .blend file. If the current .blend is not in a repository, do nothing"), - ('SELECTED_REPO', "Selected Repo", - "Communicate with the selected repository") - ], - default='CURRENT_BLEND', - update=update_active_repo_mode - ) - active_repo_idx: IntProperty( name="SVN Repositories", options=set(), update=update_active_repo_idx ) - idx_updating: BoolProperty( - name="Index is Updating", - description="Helper flag to avoid infinite looping update callbacks", - ) @property - def active_repo(self) -> SVN_repository: + def active_repo(self) -> Optional[SVN_repository]: + if not self.is_svn_installed: + return if 0 <= self.active_repo_idx <= len(self.repositories)-1: return self.repositories[self.active_repo_idx] diff --git a/scripts-blender/addons/blender_svn/props.py b/scripts-blender/addons/blender_svn/props.py index e7b8faf4..4c2d9197 100644 --- a/scripts-blender/addons/blender_svn/props.py +++ b/scripts-blender/addons/blender_svn/props.py @@ -28,33 +28,10 @@ class SVN_scene_properties(PropertyGroup): ) def get_repo(self, context) -> Optional['SVN_repository']: - """Return the current repository. - Depending on preferences, this is either the repo the current .blend file is in, - or whatever repo is selected in the preferences UI. - """ + """Return the active repository.""" prefs = get_addon_prefs(context) - if not prefs.is_svn_installed: - return + return prefs.active_repo - if prefs.active_repo_mode == 'CURRENT_BLEND': - return self.get_scene_repo(context) - else: - return prefs.active_repo - - def get_scene_repo(self, context) -> Optional['SVN_repository']: - """Return the repository of the current file, even if the add-on is - configured to another repository. - """ - prefs = get_addon_prefs(context) - if not prefs.is_svn_installed: - return - - if not self.svn_url or not self.svn_directory: - return - - for repo in prefs.repositories: - if (repo.url == self.svn_url) and (Path(repo.directory) == Path(self.svn_directory)): - return repo registry = [ SVN_scene_properties, diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index 36253d93..a4e85de7 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -305,9 +305,9 @@ class SVN_repository(PropertyGroup): ) @property - def is_cred_entered(self): + def is_cred_entered(self) -> bool: """Check if there's a username and password entered at all.""" - return self.username and self.password + return bool(self.username and self.password) authenticated: BoolProperty( name="Authenticated", diff --git a/scripts-blender/addons/blender_svn/threaded/background_process.py b/scripts-blender/addons/blender_svn/threaded/background_process.py index 32c4a6b1..17c34c84 100644 --- a/scripts-blender/addons/blender_svn/threaded/background_process.py +++ b/scripts-blender/addons/blender_svn/threaded/background_process.py @@ -44,7 +44,7 @@ class BackgroundProcess: # Displayed in the tooltip on mouse-hover in the error message when an error occurs. error_description = "SVN Error:" - debug = True + debug = False def debug_print(self, msg: str): if self.debug: @@ -82,7 +82,7 @@ class BackgroundProcess: def handle_error(self, context, error): self.output = "" self.error = error.stderr.decode() - self.is_running = False + self.stop() def process_output(self, context, prefs): """ @@ -113,7 +113,7 @@ class BackgroundProcess: repo = context.scene.svn.get_repo(context) if not repo: self.debug_print("Shutdown: Not in repo.") - self.is_running = False + self.stop() return prefs = get_addon_prefs(context) @@ -127,7 +127,7 @@ class BackgroundProcess: if self.needs_authentication and not repo.authenticated: self.debug_print("Shutdown: Authentication needed.") - self.is_running = False + self.stop() return if not self.thread or not self.thread.is_alive() and not self.output and not self.error: @@ -146,15 +146,14 @@ class BackgroundProcess: self.output = "" redraw_viewport() if self.repeat_delay == 0: - self.debug_print( - "Shutdown: Output was processed, repeat_delay==0.") - self.is_running = False + self.debug_print("Shutdown: Output was processed, repeat_delay==0.") + self.stop() return self.debug_print(f"Processed output. Waiting {self.repeat_delay}") return self.repeat_delay elif not self.thread and not self.thread.is_alive() and self.repeat_delay == 0: self.debug_print("Shutdown: Finished.\n") - self.is_running = False + self.stop() return self.debug_print(f"Tick delay: {self.tick_delay}") @@ -188,6 +187,7 @@ class BackgroundProcess: def stop(self): """Stop the process if it isn't running, by unregistering its timer function""" + self.debug_print("stop() function was called.") self.is_running = False if bpy.app.timers.is_registered(self.timer_function): # This won't work if the timer has returned None at any point, as that diff --git a/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py b/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py index f2911c2c..c9784a97 100644 --- a/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py +++ b/scripts-blender/addons/blender_svn/threaded/redraw_viewport.py @@ -7,6 +7,7 @@ class BGP_SVN_Redraw_Viewport(BackgroundProcess): repeat_delay = 1 debug = False tick_delay = 1 + needs_authentication = False def tick(self, context, prefs): redraw_viewport() diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 7837d33b..1bf34f16 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -56,13 +56,12 @@ class SVN_OT_explain_status(Operator): @bpy.app.handlers.persistent def ensure_svn_of_current_file(_scene=None): - """When opening or saving a .blend file, it's possible that the new .blend + """When opening or saving a .blend file, it's possible that the new .blend is part of an SVN repository. If this is the case, do the following: - - Initialize SVN Scene info - - Initialize Repository - - Try to authenticate + - Check if this file's repository is already in our database + - If not, create it + - Switch to that repo """ - context = bpy.context prefs = get_addon_prefs(context) prefs.is_svn_installed = check_svn_installed() @@ -71,52 +70,42 @@ def ensure_svn_of_current_file(_scene=None): scene_svn = context.scene.svn + # If we have any repository entries, make sure at least one is active. prefs.sync_repo_info_file() - if prefs.active_repo_idx == -1 and len(prefs.repositories) > 0: prefs.active_repo_idx = 0 + elif prefs.active_repo_idx > len(prefs.repositories)-1: + prefs.active_repo_idx = 0 + else: + prefs.active_repo_idx = prefs.active_repo_idx - repo = prefs.active_repo - if repo and repo.is_cred_entered and repo.authenticated: - status = Processes.get('Status') - if status and time.time() - status.timestamp_last_update < status.repeat_delay + 5: - # If the SVN Status background process has recently processed data, - # there is no need to re-initialize everything. - # This happens when a file that was already saved is saved again, - # or a file from the same repository as the previous one is loaded. - Processes.restart('Status') - return + # If the file is unsaved, nothing more to do. + if not bpy.data.filepath: + scene_svn.svn_url = "" + return + # If file is not in a repo, nothing more to do. + is_in_repo = set_scene_svn_info(context) + if not is_in_repo: + return + + # If we switched repos, reset auth flags and authenticate the new repo. for repo in prefs.repositories: # This would ideally only run when opening Blender for the first # time, but there is no app handler for that, sadly. repo.authenticated = False repo.auth_failed = False - if prefs.active_repo_mode == 'CURRENT_BLEND': - if not bpy.data.filepath: - scene_svn.svn_url = "" - return - - is_in_repo = set_scene_svn_info(context) - if not is_in_repo: - return - - repo = scene_svn.get_scene_repo(context) - if not repo: - repo = prefs.init_repo(context, scene_svn.svn_directory) - - for i, other_repo in enumerate(prefs.repositories): - if other_repo == repo: - prefs.active_repo_idx = i - + # If file is in an existing repo, we should switch over to that repo. + for i, existing_repo in enumerate(prefs.repositories): + if ( existing_repo.url == scene_svn.svn_url and + existing_repo.directory == scene_svn.svn_directory + ): + prefs.active_repo_idx = i + break else: - repo = prefs.active_repo - if not repo: - return - - if repo.is_cred_entered: - repo.authenticate(context) + # If file is in a non-existing repo, initialize that repo. + repo = prefs.init_repo(context, scene_svn.svn_directory) def set_scene_svn_info(context) -> bool: diff --git a/scripts-blender/addons/blender_svn/ui/ui_prefs.py b/scripts-blender/addons/blender_svn/ui/ui_prefs.py index 674ca8aa..b305c22a 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_prefs.py +++ b/scripts-blender/addons/blender_svn/ui/ui_prefs.py @@ -18,9 +18,6 @@ class SVN_UL_repositories(UIList): repo = item row = layout.row() - prefs = get_addon_prefs(context) - if prefs.active_repo_mode == 'CURRENT_BLEND' and repo != context.scene.svn.get_repo(context): - row.enabled = False row.label(text=repo.display_name) if not repo.dir_exists: @@ -198,10 +195,6 @@ def draw_prefs_checkout(self, context): def draw_prefs_repos(self, context) -> None: layout = self.layout - row = layout.row() - row.use_property_split = True - row.prop(self, 'active_repo_mode', expand=True) - auth_in_progress = False auth_error = False auth_proc = Processes.get('Authenticate') @@ -209,12 +202,6 @@ def draw_prefs_repos(self, context) -> None: auth_in_progress = auth_proc.is_running auth_error = auth_proc.error - if self.active_repo_mode == 'CURRENT_BLEND' and not context.scene.svn.get_repo(context): - split = layout.split(factor=0.4) - split.row() - split.row().label(text="Current file is not in a repository.") - return - repo_col = layout.column() split = repo_col.row().split() split.row().label(text="SVN Repositories:") diff --git a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py index 1bc81a38..7289b9ef 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py +++ b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py @@ -16,12 +16,7 @@ class VIEW3D_PT_svn_credentials(Panel): @classmethod def poll(cls, context): - prefs = get_addon_prefs(context) - if prefs.active_repo_mode == 'CURRENT_BLEND': - repo = context.scene.svn.get_scene_repo(context) - else: - repo = context.scene.svn.get_repo(context) - + repo = context.scene.svn.get_repo(context) return repo and not repo.authenticated def draw(self, context): -- 2.30.2 From 825cfa8a963c1c02adc92f4fe40454f14d4668dd Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 17:39:12 +0200 Subject: [PATCH 05/15] Fix having to re-auth when changing repo --- scripts-blender/addons/blender_svn/prefs.py | 13 ++++++++++- .../addons/blender_svn/repository.py | 6 ++--- .../addons/blender_svn/threaded/svn_status.py | 23 ++++--------------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index f8fc5cf7..62857b51 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -27,6 +27,17 @@ class SVN_addon_preferences(AddonPreferences): repositories: CollectionProperty(type=SVN_repository) + def init_repo_list(self): + # If we have any repository entries, make sure at least one is active. + self.sync_repo_info_file() + + if self.active_repo_idx == -1 and len(self.repositories) > 0: + self.active_repo_idx = 0 + elif self.active_repo_idx > len(self.repositories)-1: + self.active_repo_idx = 0 + else: + self.active_repo_idx = self.active_repo_idx + def init_repo(self, context, repo_path: Path or str): """Attempt to initialize a repository based on a directory. This means executing `svn info` in the repo_path to get the URL and root dir. @@ -65,7 +76,7 @@ class SVN_addon_preferences(AddonPreferences): if active_repo.authenticated: Processes.restart('Status') else: - active_repo.authenticate(context) + active_repo.authenticate() else: Processes.kill('Status') diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index a4e85de7..cb607ab4 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -282,10 +282,10 @@ class SVN_repository(PropertyGroup): if get_addon_prefs(context).loading: return - self.authenticate(context) + self.authenticate() self.update_repo_info_file(context) - def authenticate(self, context): + def authenticate(self): self.auth_failed = False if self.is_valid_svn and self.is_cred_entered: Processes.start('Authenticate') @@ -316,7 +316,7 @@ class SVN_repository(PropertyGroup): ) auth_failed: BoolProperty( name="Authentication Failed", - description="Internal flag to mark whether the last entered credentials were denied by the repo", + description="Internal flag to mark whether the last entered credentials were rejected by the repo", default=False ) diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index 1bf34f16..eeeef253 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -70,14 +70,8 @@ def ensure_svn_of_current_file(_scene=None): scene_svn = context.scene.svn - # If we have any repository entries, make sure at least one is active. - prefs.sync_repo_info_file() - if prefs.active_repo_idx == -1 and len(prefs.repositories) > 0: - prefs.active_repo_idx = 0 - elif prefs.active_repo_idx > len(prefs.repositories)-1: - prefs.active_repo_idx = 0 - else: - prefs.active_repo_idx = prefs.active_repo_idx + old_active_repo = prefs.active_repo + prefs.init_repo_list() # If the file is unsaved, nothing more to do. if not bpy.data.filepath: @@ -89,23 +83,16 @@ def ensure_svn_of_current_file(_scene=None): if not is_in_repo: return - # If we switched repos, reset auth flags and authenticate the new repo. - for repo in prefs.repositories: - # This would ideally only run when opening Blender for the first - # time, but there is no app handler for that, sadly. - repo.authenticated = False - repo.auth_failed = False - # If file is in an existing repo, we should switch over to that repo. for i, existing_repo in enumerate(prefs.repositories): if ( existing_repo.url == scene_svn.svn_url and - existing_repo.directory == scene_svn.svn_directory + existing_repo.directory == scene_svn.svn_directory and + existing_repo != old_active_repo ): prefs.active_repo_idx = i - break else: # If file is in a non-existing repo, initialize that repo. - repo = prefs.init_repo(context, scene_svn.svn_directory) + prefs.init_repo(context, scene_svn.svn_directory) def set_scene_svn_info(context) -> bool: -- 2.30.2 From 14cfc176ca4abaa8b8f0b8e36d96646b75c20dc2 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Mon, 24 Jul 2023 17:40:23 +0200 Subject: [PATCH 06/15] Hide Debug Mode --- scripts-blender/addons/blender_svn/ui/ui_prefs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts-blender/addons/blender_svn/ui/ui_prefs.py b/scripts-blender/addons/blender_svn/ui/ui_prefs.py index b305c22a..c33536b9 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_prefs.py +++ b/scripts-blender/addons/blender_svn/ui/ui_prefs.py @@ -205,9 +205,9 @@ def draw_prefs_repos(self, context) -> None: repo_col = layout.column() split = repo_col.row().split() split.row().label(text="SVN Repositories:") - row = split.row() - row.alignment = 'RIGHT' - row.prop(self, 'debug_mode') + # row = split.row() + # row.alignment = 'RIGHT' + # row.prop(self, 'debug_mode') repo_col.enabled = not auth_in_progress -- 2.30.2 From e71d4182eefaa0a601ab0a6d8732f19903b30e95 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 13:18:13 +0200 Subject: [PATCH 07/15] Cleanup: Split up ui_prefs It was hard to find where the code for the repo list add/remove operators were. Filenames are more specific and accurate now. --- scripts-blender/addons/blender_svn/prefs.py | 31 ++- .../addons/blender_svn/ui/__init__.py | 4 +- .../ui/{ui_prefs.py => ui_repo_list.py} | 215 ++++++++---------- 3 files changed, 130 insertions(+), 120 deletions(-) rename scripts-blender/addons/blender_svn/ui/{ui_prefs.py => ui_repo_list.py} (77%) diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index 62857b51..ee91f66e 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -3,12 +3,13 @@ # (c) 2022, Blender Foundation - Demeter Dzadik from typing import Optional, Any, Set, Tuple, List +import platform import bpy from bpy.props import IntProperty, CollectionProperty, BoolProperty, EnumProperty from bpy.types import AddonPreferences -from .ui import ui_prefs +from .ui.ui_repo_list import draw_checkout, draw_repo_list from .repository import SVN_repository from .svn_info import get_svn_info import json @@ -156,7 +157,33 @@ class SVN_addon_preferences(AddonPreferences): self.load_repo_info_from_file() self.save_repo_info_to_file() - draw = ui_prefs.draw_prefs + def draw(self, context): + if not self.is_svn_installed: + draw_prefs_no_svn(self, context) + return + + if self.checkout_mode: + draw_checkout(self, context) + 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() + if system == "Windows": + terminal = "command line (cmd.exe)" + url = "https://subversion.apache.org/packages.html#windows" + elif system == "Darwin": + terminal = "Mac terminal" + url = "https://subversion.apache.org/packages.html#osx" + + layout = self.layout + col = layout.column() + col.alert=True + col.label(text="Please ensure that Subversion (aka. SVN) is installed on your system.") + col.label(text=f"Typing `svn` into the {terminal} should yield a result.") + layout.operator("wm.url_open", icon='URL', text='Open Subversion Distribution Page').url=url registry = [ diff --git a/scripts-blender/addons/blender_svn/ui/__init__.py b/scripts-blender/addons/blender_svn/ui/__init__.py index 10daf702..716a8ec5 100644 --- a/scripts-blender/addons/blender_svn/ui/__init__.py +++ b/scripts-blender/addons/blender_svn/ui/__init__.py @@ -3,7 +3,7 @@ from . import ( ui_sidebar, ui_filebrowser, ui_log, - ui_prefs, + ui_repo_list, ui_outdated_warning, ui_context_menus ) @@ -13,7 +13,7 @@ modules = [ ui_sidebar, ui_filebrowser, ui_log, - ui_prefs, + ui_repo_list, ui_outdated_warning, ui_context_menus ] diff --git a/scripts-blender/addons/blender_svn/ui/ui_prefs.py b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py similarity index 77% rename from scripts-blender/addons/blender_svn/ui/ui_prefs.py rename to scripts-blender/addons/blender_svn/ui/ui_repo_list.py index c33536b9..94055920 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_prefs.py +++ b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later # (c) 2023, Blender Foundation - Demeter Dzadik -from pathlib import Path +import platform from bpy.types import UIList, Operator, Menu from bpy_extras.io_utils import ImportHelper @@ -10,11 +10,14 @@ from ..util import get_addon_prefs from .ui_log import draw_svn_log, is_log_useful from .ui_file_list import draw_repo_file_list, draw_process_info from ..threaded.background_process import Processes -import platform + +from pathlib import Path class SVN_UL_repositories(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 + ): repo = item row = layout.row() @@ -27,6 +30,7 @@ class SVN_UL_repositories(UIList): class SVN_OT_repo_add(Operator, ImportHelper): """Add a repository to the list""" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_idname = "svn.repo_add" @@ -45,7 +49,9 @@ class SVN_OT_repo_add(Operator, ImportHelper): repo = prefs.init_repo(context, path) except Exception as e: self.report( - {'ERROR'}, "Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.") + {'ERROR'}, + "Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.", + ) print(e) return {'CANCELLED'} if not repo: @@ -62,6 +68,7 @@ class SVN_OT_repo_add(Operator, ImportHelper): class SVN_OT_repo_remove(Operator): """Remove a repository from the list""" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_idname = "svn.repo_remove" @@ -90,109 +97,14 @@ class SVN_MT_add_repo(Menu): def draw(self, context): layout = self.layout layout.operator( - "svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER') - layout.operator("svn.checkout_initiate", - text="Create New Checkout", icon='URL').create = True + "svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER' + ) + layout.operator( + "svn.checkout_initiate", text="Create New Checkout", icon='URL' + ).create = True -def draw_prefs(self, context): - if not self.is_svn_installed: - draw_prefs_no_svn(self, context) - return - - if self.checkout_mode: - draw_prefs_checkout(self, context) - else: - draw_prefs_repos(self, context) - - -def draw_prefs_no_svn(self, context): - terminal, url = "terminal", "https://subversion.apache.org/packages.html" - system = platform.system() - if system == "Windows": - terminal = "command line (cmd.exe)" - url = "https://subversion.apache.org/packages.html#windows" - elif system == "Darwin": - terminal = "Mac terminal" - url = "https://subversion.apache.org/packages.html#osx" - - layout = self.layout - col = layout.column() - col.alert=True - col.label(text="Please ensure that Subversion (aka. SVN) is installed on your system.") - col.label(text=f"Typing `svn` into the {terminal} should yield a result.") - layout.operator("wm.url_open", icon='URL', text='Open Subversion Distribution Page').url=url - - -def draw_prefs_checkout(self, context): - def get_terminal_howto(): - msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console." - msg_linux = "If you don't, quit Blender and re-launch it from a terminal." - msg_mac = msg_linux - - system = platform.system() - if system == "Windows": - return msg_windows - elif system == "Linux": - return msg_linux - elif system == "Darwin": - return msg_mac - - layout = self.layout - col = layout.column() - col.alert = True - - col.label(text="IMPORTANT! ", icon='ERROR') - col.label(text="Make sure you have Blender's terminal open!") - col.label(text=get_terminal_howto()) - col.separator() - col.label( - text="Downloading a repository can take a long time, and the UI will be locked.") - col.label( - text="Without a terminal, you won't be able to track the progress of the checkout.") - col.separator() - - col = layout.column() - col.label( - text="To interrupt the checkout, you can press Ctrl+C in the terminal.", icon='INFO') - col.label( - text="You can resume it by re-running this operation, or with the SVN Update button.", icon='INFO') - col.separator() - - prefs = get_addon_prefs(context) - repo = prefs.repositories[-1] - col.prop(repo, 'directory') - for other_repo in prefs.repositories: - if other_repo == repo: - continue - if other_repo.directory == repo.directory: - row = col.row() - row.alert = True - row.label( - text="A repository at this filepath is already specified.", icon='ERROR') - break - - col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER') - col.prop(repo, 'url', icon='URL') - for other_repo in prefs.repositories: - if other_repo == repo: - continue - if other_repo.url == repo.url: - sub = col.column() - sub.alert = True - sub.label(text="A repository with this URL is already specified.") - sub.label( - text="If you're sure you want to checkout another copy of the repo, feel free to proceed.") - break - col.prop(repo, 'username', icon='USER') - col.prop(repo, 'password', icon='LOCKED') - - op_row = layout.row() - op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK') - op_row.operator('svn.checkout_cancel', text="Cancel", icon="X") - - -def draw_prefs_repos(self, context) -> None: +def draw_repo_list(self, context, draw_files=True, draw_log=True) -> None: layout = self.layout auth_in_progress = False @@ -228,7 +140,7 @@ def draw_prefs_repos(self, context) -> None: if len(self.repositories) == 0: return - if self.active_repo_idx-1 > len(self.repositories): + if self.active_repo_idx - 1 > len(self.repositories): return if not self.active_repo: return @@ -247,12 +159,12 @@ def draw_prefs_repos(self, context) -> None: draw_repo_error(layout, "Directory is not an SVN repository.") split = layout.split(factor=0.24) split.row() - split.row().operator("svn.checkout_initiate", - text="Create New Checkout", icon='URL').create = False + split.row().operator( + "svn.checkout_initiate", text="Create New Checkout", icon='URL' + ).create = False return if not self.active_repo.authenticated and not auth_in_progress and not auth_error: - draw_repo_error( - layout, "Repository not authenticated. Enter your credentials.") + draw_repo_error(layout, "Repository not authenticated. Enter your credentials.") return if len(self.repositories) > 0 and self.active_repo.authenticated: @@ -274,9 +186,80 @@ def draw_repo_error(layout, message): col.label(text=message, icon='ERROR') -registry = [ - SVN_UL_repositories, - SVN_OT_repo_add, - SVN_OT_repo_remove, - SVN_MT_add_repo -] +def draw_checkout(self, context): + def get_terminal_howto(): + msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console." + msg_linux = "If you don't, quit Blender and re-launch it from a terminal." + msg_mac = msg_linux + + system = platform.system() + if system == "Windows": + return msg_windows + elif system == "Linux": + return msg_linux + elif system == "Darwin": + return msg_mac + + layout = self.layout + col = layout.column() + col.alert = True + + col.label(text="IMPORTANT! ", icon='ERROR') + col.label(text="Make sure you have Blender's terminal open!") + col.label(text=get_terminal_howto()) + col.separator() + col.label( + text="Downloading a repository can take a long time, and the UI will be locked." + ) + col.label( + text="Without a terminal, you won't be able to track the progress of the checkout." + ) + col.separator() + + col = layout.column() + col.label( + text="To interrupt the checkout, you can press Ctrl+C in the terminal.", + icon='INFO', + ) + col.label( + text="You can resume it by re-running this operation, or with the SVN Update button.", + icon='INFO', + ) + col.separator() + + prefs = get_addon_prefs(context) + repo = prefs.repositories[-1] + col.prop(repo, 'directory') + for other_repo in prefs.repositories: + if other_repo == repo: + continue + if other_repo.directory == repo.directory: + row = col.row() + row.alert = True + row.label( + text="A repository at this filepath is already specified.", icon='ERROR' + ) + break + + col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER') + col.prop(repo, 'url', icon='URL') + for other_repo in prefs.repositories: + if other_repo == repo: + continue + if other_repo.url == repo.url: + sub = col.column() + sub.alert = True + sub.label(text="A repository with this URL is already specified.") + sub.label( + text="If you're sure you want to checkout another copy of the repo, feel free to proceed." + ) + break + col.prop(repo, 'username', icon='USER') + col.prop(repo, 'password', icon='LOCKED') + + op_row = layout.row() + op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK') + op_row.operator('svn.checkout_cancel', text="Cancel", icon="X") + + +registry = [SVN_UL_repositories, SVN_OT_repo_add, SVN_OT_repo_remove, SVN_MT_add_repo] -- 2.30.2 From c368c747720fbd5dedf924c8f8929eb1e7c375d8 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 13:40:24 +0200 Subject: [PATCH 08/15] Fix error when adding an existing repo --- scripts-blender/addons/blender_svn/ui/ui_repo_list.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py index 94055920..4b902bcd 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py @@ -13,7 +13,6 @@ from ..threaded.background_process import Processes from pathlib import Path - class SVN_UL_repositories(UIList): def draw_item( self, context, layout, data, item, icon, active_data, active_propname @@ -41,7 +40,16 @@ class SVN_OT_repo_add(Operator, ImportHelper): repos = prefs.repositories path = Path(self.filepath) + if not path.exists(): + # It's unlikely that a path that the user JUST BROWSED doesn't exist. + # So, this actually happens when the user leaves a filename in the + # file browser text box while trying to select the folder... + # Basically, Blender is dumb, and it will add that filename to the + # end of the browsed path. We need to discard that. + path = path.parent if path.is_file(): + # Maybe the user actually did select an existing file in the repo. + # We still want to discard the filename. path = path.parent existing_repos = repos[:] -- 2.30.2 From 84bfa346203bbc4ed2c5a787ac2ed6d3b8b2ac80 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 13:40:38 +0200 Subject: [PATCH 09/15] Bring back the debug_mode toggle, but secret --- scripts-blender/addons/blender_svn/repository.py | 1 + scripts-blender/addons/blender_svn/ui/ui_repo_list.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index cb607ab4..8f20cfc7 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -247,6 +247,7 @@ class SVN_repository(PropertyGroup): @property def is_valid_svn(self): 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? root_dir, base_url = get_svn_info(self.directory) return ( dir_path.exists() and diff --git a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py index 4b902bcd..b0b8e677 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py @@ -125,9 +125,11 @@ def draw_repo_list(self, context, draw_files=True, draw_log=True) -> None: repo_col = layout.column() split = repo_col.row().split() split.row().label(text="SVN Repositories:") - # row = split.row() - # row.alignment = 'RIGHT' - # row.prop(self, 'debug_mode') + + # Secret debug toggle (invisible, to the right of the SVN Repositories label.) + row = split.row() + row.alignment = 'RIGHT' + row.prop(self, 'debug_mode', text="", icon='BLANK1', emboss=False) repo_col.enabled = not auth_in_progress -- 2.30.2 From f461661bb841a6f5bccccf7c15a7ff73a600aace Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 15:21:30 +0200 Subject: [PATCH 10/15] Add UX for trying to commit during a conflict --- .../blender_svn/operators/simple_commands.py | 1 + .../blender_svn/operators/svn_commit.py | 27 ++++++++++++++----- .../addons/blender_svn/repository.py | 5 ++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 1f473307..7a7fd610 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -396,6 +396,7 @@ class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator): col.alert = True col.label(text="Choose which version of the file to keep.") col.row().prop(self, 'resolve_method', expand=True) + col.separator() if self.resolve_method == 'mine-full': col.label(text="Local changes will be kept.") col.label( diff --git a/scripts-blender/addons/blender_svn/operators/svn_commit.py b/scripts-blender/addons/blender_svn/operators/svn_commit.py index 90a9a9cd..0ec60eec 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_commit.py +++ b/scripts-blender/addons/blender_svn/operators/svn_commit.py @@ -40,6 +40,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): bl_options = {'INTERNAL'} bl_property = "first_line" # Focus the text input box + popup_width = 600 + # The first line of the commit message needs to be an operator property in order # for us to be able to focus the input box automatically when the window pops up # (see bl_property above) @@ -92,7 +94,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): for f in repo.external_files: f.include_in_commit = False for f in self.get_committable_files(context): - f.include_in_commit = True + if not f.will_conflict: + f.include_in_commit = True return super().invoke(context, event) @@ -108,20 +111,30 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): row.label(text="Status") for file in files: row = layout.row() - row.prop(file, "include_in_commit", text=file.name) + split = row.split() + checkbox_ui = split.row() + status_ui = split.row() + checkbox_ui.prop(file, "include_in_commit", text=file.name) text = file.status_name icon = file.status_icon - if file == repo.current_blend_file and self.is_file_really_dirty: - split = row.split(factor=0.7) - row = split.row() - row.alert = True + if file.will_conflict: + # We don't want to conflict-resolve during a commit, it's + # confusing. User should resolve this as a separate step. + checkbox_ui.enabled = False + text = "Conflicting" + status_ui.alert = True + icon = 'ERROR' + elif file == repo.current_blend_file and self.is_file_really_dirty: + split = status_ui.split(factor=0.7) + status_ui = split.row() + status_ui.alert = True text += " but not saved!" icon = 'ERROR' op_row = split.row() op_row.alignment = 'LEFT' op_row.operator('svn.save_during_commit', icon='FILE_BLEND', text="Save") - row.label(text=text, icon=icon) + status_ui.label(text=text, icon=icon) row = layout.row() row.label(text="Commit message:") diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index 8f20cfc7..65be15eb 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -48,6 +48,10 @@ class SVN_file(PropertyGroup): default="none", options=set() ) + @property + def will_conflict(self): + return self.status != 'normal' and self.repos_status != 'none' + status_prediction_type: EnumProperty( name="Status Predicted By Process", items=[ @@ -374,6 +378,7 @@ class SVN_repository(PropertyGroup): return i, log def get_latest_revision_of_file(self, svn_path: str) -> int: + """Return the revision number of the last log entry that affects the given file.""" svn_path = str(svn_path) for log in reversed(self.log): for changed_file in log.changed_files: -- 2.30.2 From df6848fe56da4ab0e99c26650e2561177b0abf8a Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 16:40:35 +0200 Subject: [PATCH 11/15] Bring back filebrowser support (simplified) --- scripts-blender/addons/blender_svn/prefs.py | 13 ++++--- .../addons/blender_svn/repository.py | 34 +++---------------- .../addons/blender_svn/ui/ui_context_menus.py | 3 +- .../addons/blender_svn/ui/ui_file_list.py | 10 +++++- .../addons/blender_svn/ui/ui_filebrowser.py | 29 +++++----------- .../addons/blender_svn/ui/ui_log.py | 27 +++++++++------ .../addons/blender_svn/ui/ui_repo_list.py | 12 +++---- .../addons/blender_svn/ui/ui_sidebar.py | 5 ++- 8 files changed, 54 insertions(+), 79 deletions(-) diff --git a/scripts-blender/addons/blender_svn/prefs.py b/scripts-blender/addons/blender_svn/prefs.py index ee91f66e..1078f4c8 100644 --- a/scripts-blender/addons/blender_svn/prefs.py +++ b/scripts-blender/addons/blender_svn/prefs.py @@ -62,6 +62,12 @@ class SVN_addon_preferences(AddonPreferences): return repo + checkout_mode: BoolProperty( + name="Checkout In Progress", + description="Internal flag to indicate that the user is currently trying to create a new checkout", + default=False + ) + def update_active_repo_idx(self, context): if len(self.repositories) == 0: return @@ -81,13 +87,6 @@ class SVN_addon_preferences(AddonPreferences): else: Processes.kill('Status') - - checkout_mode: BoolProperty( - name="Checkout In Progress", - description="Internal flag to indicate that the user is currently trying to create a new checkout", - default=False - ) - active_repo_idx: IntProperty( name="SVN Repositories", options=set(), diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index 65be15eb..f069992a 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -72,18 +72,6 @@ class SVN_file(PropertyGroup): options=set() ) - @property - def absolute_path(self) -> Path: - """Return absolute path on the file system.""" - scene = self.id_data - svn = scene.svn - return Path(svn.svn_directory).joinpath(Path(self.svn_path)) - - @property - def relative_path(self) -> str: - """Return relative path with Blender's path conventions.""" - return bpy.path.relpath(self.absolute_path) - @property def is_outdated(self): return self.repos_status == 'modified' and self.status == 'normal' @@ -347,10 +335,6 @@ class SVN_repository(PropertyGroup): name="SVN Log", options=set() ) - log_active_index_filebrowser: IntProperty( - name="SVN Log", - options=set() - ) reload_svn_log = svn_log.reload_svn_log @@ -365,13 +349,6 @@ class SVN_repository(PropertyGroup): except IndexError: return None - @property - def active_log_filebrowser(self): - try: - return self.log[self.log_active_index_filebrowser] - except IndexError: - return None - def get_log_by_revision(self, revision: int) -> Tuple[int, SVN_log]: for i, log in enumerate(self.log): if log.revision_number == revision: @@ -457,16 +434,15 @@ class SVN_repository(PropertyGroup): space = context.space_data if space and space.type == 'FILE_BROWSER': - # Set the active file in the file browser to whatever was selected in the SVN Files panel. - self.log_active_index_filebrowser = latest_rev-1 - - space.params.directory = self.active_file.absolute_path.parent.as_posix().encode() + space.params.directory = Path(self.active_file.absolute_path).parent.as_posix().encode() space.params.filename = self.active_file.name.encode() space.deselect_all() - space.activate_file_by_relative_path( + # Set the active file in the file browser to whatever was selected + # in the SVN Files panel. + space.activate_file_by_relative_path( # This doesn't actually work, due to what I assume is a bug. relative_path=self.active_file.name) - Processes.start('Activate File') + Processes.start('Activate File') # This is my work-around. # Set the filter flag of the log entries based on whether they affect the active file or not. self.log.foreach_set( diff --git a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py index 59d9f12a..6e509d4d 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_context_menus.py +++ b/scripts-blender/addons/blender_svn/ui/ui_context_menus.py @@ -46,12 +46,11 @@ def svn_log_list_context_menu(self: UIList, context: Context) -> None: if not check_context_match(context, 'ui_list', 'SVN_UL_log'): return - is_filebrowser = context.space_data.type == 'FILE_BROWSER' layout = self.layout layout.separator() repo = context.scene.svn.get_repo(context) - active_log = repo.active_log_filebrowser if is_filebrowser else repo.active_log + active_log = repo.active_log layout.operator("svn.update_all", text=f"Revert Repository To r{active_log.revision_number}").revision = active_log.revision_number layout.separator() diff --git a/scripts-blender/addons/blender_svn/ui/ui_file_list.py b/scripts-blender/addons/blender_svn/ui/ui_file_list.py index 23a47326..2ff6c705 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_file_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_file_list.py @@ -186,10 +186,18 @@ def draw_process_info(context, layout): ", ".join([p.name for p in Processes.running_processes])) -def draw_repo_file_list(context, layout, repo): +def draw_file_list(context, layout): + prefs = get_addon_prefs(context) + repo = prefs.active_repo if not repo: return + if not repo.authenticated: + row = layout.row() + row.alert=True + row.label(text="Repository is not authenticated.", icon='ERROR') + return + main_col = layout.column() main_row = main_col.row() split = main_row.split(factor=0.6) diff --git a/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py b/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py index 7b2a5a25..15dabef5 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py +++ b/scripts-blender/addons/blender_svn/ui/ui_filebrowser.py @@ -4,11 +4,10 @@ from bpy.types import Panel from bl_ui.space_filebrowser import FileBrowserPanel -from .ui_log import draw_svn_log -from .ui_file_list import draw_repo_file_list +from .ui_log import draw_svn_log, is_log_useful +from .ui_file_list import draw_file_list from ..util import get_addon_prefs - class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOLS' @@ -20,47 +19,37 @@ class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel): if not super().poll(context): return False - repo = context.scene.svn.get_repo(context) - if not repo: - return False - - return repo.is_filebrowser_directory_in_repo(context) + prefs = get_addon_prefs(context) + return prefs.active_repo and prefs.active_repo.authenticated def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False - # TODO: Get repository of the current file browser's directory. - prefs = get_addon_prefs(context) - if len(prefs.repositories) > 0: - repo = prefs.active_repo - draw_repo_file_list(context, layout, repo) + draw_file_list(context, layout) class FILEBROWSER_PT_SVN_log(FileBrowserPanel, Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOLS' bl_category = "Bookmarks" - bl_label = "SVN Log" + bl_parent_id = "FILEBROWSER_PT_SVN_files" + bl_label = "Revision History" @classmethod def poll(cls, context): if not super().poll(context): return False - repo = context.scene.svn.get_repo(context) - if not repo: - return False - - return repo.get_filebrowser_active_file(context) + return is_log_useful(context) def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False - draw_svn_log(context, layout, file_browser=True) + draw_svn_log(context, layout) registry = [ diff --git a/scripts-blender/addons/blender_svn/ui/ui_log.py b/scripts-blender/addons/blender_svn/ui/ui_log.py index a09276a6..3fe7d2f2 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_log.py +++ b/scripts-blender/addons/blender_svn/ui/ui_log.py @@ -3,6 +3,7 @@ from bpy.props import IntProperty, BoolProperty from bpy.types import UIList, Panel, Operator +from ..util import get_addon_prefs class SVN_UL_log(UIList): @@ -21,9 +22,7 @@ class SVN_UL_log(UIList): num, auth, date, msg = layout_log_split(layout.row()) - is_filebrowser = context.space_data.type == 'FILE_BROWSER' - active_file = svn.get_filebrowser_active_file( - context) if is_filebrowser else svn.active_file + active_file = svn.active_file num.label(text=str(log_entry.revision_number)) if item.revision_number == active_file.revision: num.operator('svn.tooltip_log', text="", icon='LAYER_ACTIVE', @@ -88,9 +87,13 @@ class SVN_UL_log(UIList): toggle=True, icon='ALIGN_JUSTIFY') -def is_log_useful(context): - repo = context.scene.svn.get_repo(context) - if not repo: +def is_log_useful(context) -> bool: + """Return whether the log has any useful info to display.""" + + prefs = get_addon_prefs(context) + repo = prefs.active_repo + + if not repo or not repo.authenticated: return False if len(repo.log) == 0 or len(repo.external_files) == 0: @@ -124,7 +127,7 @@ class VIEW3D_PT_svn_log(Panel): layout.use_property_split = True layout.use_property_decorate = False - draw_svn_log(context, layout, file_browser=False) + draw_svn_log(context, layout) def layout_log_split(layout): @@ -143,23 +146,25 @@ def layout_log_split(layout): return num, auth, date, msg -def draw_svn_log(context, layout, file_browser: bool): +def draw_svn_log(context, layout): num, auth, date, msg = layout_log_split(layout.row()) num.label(text="Rev. #") auth.label(text="Author") date.label(text="Date") msg.label(text="Message") - repo = context.scene.svn.get_repo(context) + + prefs = get_addon_prefs(context) + repo = prefs.active_repo layout.template_list( "SVN_UL_log", "svn_log", repo, "log", repo, - "log_active_index_filebrowser" if file_browser else "log_active_index", + "log_active_index", ) - active_log = repo.active_log_filebrowser if file_browser else repo.active_log + active_log = repo.active_log if not active_log: return layout.label(text="Revision Date: " + active_log.revision_date) diff --git a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py index b0b8e677..d11ce989 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_repo_list.py +++ b/scripts-blender/addons/blender_svn/ui/ui_repo_list.py @@ -8,7 +8,7 @@ from bpy_extras.io_utils import ImportHelper from ..util import get_addon_prefs from .ui_log import draw_svn_log, is_log_useful -from .ui_file_list import draw_repo_file_list, draw_process_info +from .ui_file_list import draw_file_list, draw_process_info from ..threaded.background_process import Processes from pathlib import Path @@ -112,7 +112,7 @@ class SVN_MT_add_repo(Menu): ).create = True -def draw_repo_list(self, context, draw_files=True, draw_log=True) -> None: +def draw_repo_list(self, context) -> None: layout = self.layout auth_in_progress = False @@ -179,13 +179,13 @@ def draw_repo_list(self, context, draw_files=True, draw_log=True) -> None: if len(self.repositories) > 0 and self.active_repo.authenticated: layout.separator() - layout.label(text="Files: ") - draw_repo_file_list(context, layout, self.active_repo) + layout.label(text="SVN Files: ") + draw_file_list(context, layout) if is_log_useful(context): layout.separator() - layout.label(text="Log: ") - draw_svn_log(context, layout, file_browser=False) + layout.label(text="Revision History: ") + draw_svn_log(context, layout) def draw_repo_error(layout, message): diff --git a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py index 7289b9ef..a0e47123 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_sidebar.py +++ b/scripts-blender/addons/blender_svn/ui/ui_sidebar.py @@ -4,7 +4,7 @@ from bpy.types import Panel from ..util import get_addon_prefs -from .ui_file_list import draw_repo_file_list, draw_process_info +from .ui_file_list import draw_file_list, draw_process_info class VIEW3D_PT_svn_credentials(Panel): @@ -53,9 +53,8 @@ class VIEW3D_PT_svn_files(Panel): layout.use_property_split = True layout.use_property_decorate = False - repo = context.scene.svn.get_repo(context) draw_process_info(context, layout) - draw_repo_file_list(context, layout, repo) + draw_file_list(context, layout) registry = [ -- 2.30.2 From 3da7d983a1c90624c04f0f1b92eb3245e5de713f Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 25 Jul 2023 16:58:16 +0200 Subject: [PATCH 12/15] Keep the "file is outdated" warning if no reload Previously, if the user loads an old version of a file, then updates to the latest again without reloading, the "this file is outdated" warning will disappear, which is slightly misleading. This could still happen though, if there are two instances of Blender open, and you mess with the file that's open in one, from the other. Can't help that. --- .../addons/blender_svn/operators/simple_commands.py | 2 ++ scripts-blender/addons/blender_svn/props.py | 9 +++++++-- .../addons/blender_svn/ui/ui_outdated_warning.py | 5 +---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 7a7fd610..4f504077 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -132,6 +132,8 @@ class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator): super().execute(context) if self.reload_file: bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False) + else: + context.scene.svn.file_is_outdated = True return {'FINISHED'} diff --git a/scripts-blender/addons/blender_svn/props.py b/scripts-blender/addons/blender_svn/props.py index 4c2d9197..f3d97b34 100644 --- a/scripts-blender/addons/blender_svn/props.py +++ b/scripts-blender/addons/blender_svn/props.py @@ -3,10 +3,9 @@ # (c) 2022, Blender Foundation - Demeter Dzadik from .util import get_addon_prefs -from bpy.props import StringProperty, PointerProperty +from bpy.props import StringProperty, PointerProperty, BoolProperty from bpy.types import PropertyGroup import bpy -from pathlib import Path from typing import Optional, Dict, Any, List, Tuple, Set from . import wheels # This will load the dateutil and BAT wheel files. @@ -27,6 +26,12 @@ class SVN_scene_properties(PropertyGroup): description="Absolute directory path of the SVN repository's root in the file system", ) + file_is_outdated: BoolProperty( + name="File Is Outdated", + description="Set to True when downloading a newer version of this file without reloading it, so that the warning in the UI can persist. This won't work in some cases involving multiple running Blender instances", + default=False + ) + def get_repo(self, context) -> Optional['SVN_repository']: """Return the active repository.""" prefs = get_addon_prefs(context) diff --git a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py index 8f6afb3d..eef00392 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py +++ b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py @@ -17,9 +17,6 @@ def draw_outdated_file_warning(self, context): # If the current file is not in an SVN repository. return - if current_file.status == 'normal' and current_file.repos_status == 'none': - return - layout = self.layout row = layout.row() row.alert = True @@ -27,7 +24,7 @@ def draw_outdated_file_warning(self, context): if current_file.status == 'conflicted': row.operator('svn.resolve_conflict', text="SVN: This .blend file is conflicted.", icon='ERROR') - elif current_file.repos_status != 'none': + elif current_file.repos_status != 'none' or context.scene.svn.file_is_outdated: op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file is outdated.", icon='ERROR') op.file_rel_path = repo.current_blend_file.svn_path -- 2.30.2 From 890e1b0e76ecbc2056ce2fedf8902e2687071f82 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 1 Aug 2023 11:55:05 +0200 Subject: [PATCH 13/15] Fix active log index getting reset all the time --- .../addons/blender_svn/repository.py | 17 ++++++++++++----- .../addons/blender_svn/threaded/svn_status.py | 15 --------------- scripts-blender/addons/blender_svn/ui/ui_log.py | 5 +---- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/scripts-blender/addons/blender_svn/repository.py b/scripts-blender/addons/blender_svn/repository.py index f069992a..f33cf891 100644 --- a/scripts-blender/addons/blender_svn/repository.py +++ b/scripts-blender/addons/blender_svn/repository.py @@ -425,7 +425,13 @@ class SVN_repository(PropertyGroup): def update_active_file(self, context): """When user clicks on a different file, the latest log entry of that file - should become the active log entry.""" + should become the active log entry. + NOTE: Try to only trigger this on explicit user actions! + """ + + if self.external_files_active_index == self.prev_external_files_active_index: + return + self.prev_external_files_active_index = self.external_files_active_index latest_rev = self.get_latest_revision_of_file( self.active_file.svn_path) @@ -451,6 +457,11 @@ class SVN_repository(PropertyGroup): for log_entry in self.log] ) + prev_external_files_active_index: IntProperty( + name="Previous Active Index", + description="Internal value to avoid triggering the update callback unnecessarily", + options=set() + ) external_files_active_index: IntProperty( name="File List", description="Files tracked by SVN", @@ -521,10 +532,6 @@ class SVN_repository(PropertyGroup): if len(self.external_files) == 0: return - if self.active_file.show_in_filelist: - # Trigger the update callback, because that refreshes the SVN Log UI. - self.external_files_active_index = self.external_files_active_index - return # Make sure the active file isn't now being filtered out. # If it is, change the active file to the first visible one. diff --git a/scripts-blender/addons/blender_svn/threaded/svn_status.py b/scripts-blender/addons/blender_svn/threaded/svn_status.py index eeeef253..f9468acf 100644 --- a/scripts-blender/addons/blender_svn/threaded/svn_status.py +++ b/scripts-blender/addons/blender_svn/threaded/svn_status.py @@ -228,21 +228,6 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]): if not file_entry.exists: new_files_on_repo.add((file_entry.svn_path, repos_status)) - # if file_entry.status_prediction_type == 'SKIP_ONCE': - # # File status was predicted by a local svn file operation, - # # so we should ignore this status update and reset the flag. - # # The file status will be updated on the next status update. - # # This is because this status update was initiated before the file's - # # status was predicted, so the prediction is likely to be correct, - # # and the status we have here is likely to be outdated. - # file_entry.status_prediction_type = 'SKIPPED_ONCE' - # continue - # elif file_entry.status_prediction_type not in {'NONE', 'SKIPPED_ONCE'}: - # # We wait for `svn up/commit` background processes to finish and - # # set the predicted flag to SKIP_ONCE. Until then, we ignore status - # # updates on files that are being updated or committed. - # continue - if entry_existed and (file_entry.repos_status == 'none' and repos_status != 'none'): new_files_on_repo.add((file_entry.svn_path, repos_status)) diff --git a/scripts-blender/addons/blender_svn/ui/ui_log.py b/scripts-blender/addons/blender_svn/ui/ui_log.py index 3fe7d2f2..ea6a10b0 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_log.py +++ b/scripts-blender/addons/blender_svn/ui/ui_log.py @@ -195,10 +195,7 @@ def execute_tooltip_log(self, context): repo = context.scene.svn.get_repo(context) tup = repo.get_log_by_revision(self.log_rev) if tup: - if context.area.type == 'FILE_BROWSER': - repo.log_active_index_filebrowser = tup[0] - else: - repo.log_active_index = tup[0] + repo.log_active_index = tup[0] return {'FINISHED'} -- 2.30.2 From b881cffd91d8153616b25b6892989e11578b8e76 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 1 Aug 2023 13:34:25 +0200 Subject: [PATCH 14/15] Fix SVN Up remembering revision number --- scripts-blender/addons/blender_svn/operators/svn_update.py | 3 +++ .../addons/blender_svn/threaded/background_process.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/scripts-blender/addons/blender_svn/operators/svn_update.py b/scripts-blender/addons/blender_svn/operators/svn_update.py index 161ad625..a066da13 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_update.py +++ b/scripts-blender/addons/blender_svn/operators/svn_update.py @@ -44,11 +44,14 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator): current_blend = repo.current_blend_file if self.revision == 0: if current_blend and current_blend.repos_status != 'none': + # If the current file will be modified, warn user. self.file_rel_path = current_blend.svn_path return context.window_manager.invoke_props_dialog(self, width=500) else: for f in repo.external_files: if f.status in ['modified', 'added', 'conflicted', 'deleted', 'missing', 'unversioned']: + # If user wants to check out an older version of the repo but + # there are uncommitted local changes to any files, warn user. return context.window_manager.invoke_props_dialog(self, width=500) return self.execute(context) diff --git a/scripts-blender/addons/blender_svn/threaded/background_process.py b/scripts-blender/addons/blender_svn/threaded/background_process.py index 17c34c84..968336e4 100644 --- a/scripts-blender/addons/blender_svn/threaded/background_process.py +++ b/scripts-blender/addons/blender_svn/threaded/background_process.py @@ -239,6 +239,8 @@ class ProcessManager: process = self.processes.get(proc_name, None) if process: process.start() + for key, value in kwargs.items(): + setattr(process, key, value) return else: for subcl in get_recursive_subclasses(BackgroundProcess): -- 2.30.2 From 8c8d8bfa0bd40e0e00ab82a2cdb2781104185135 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Tue, 1 Aug 2023 13:55:40 +0200 Subject: [PATCH 15/15] Improve communication of file_is_outdated flag --- scripts-blender/addons/blender_svn/operators/simple_commands.py | 2 +- scripts-blender/addons/blender_svn/operators/svn_commit.py | 1 - scripts-blender/addons/blender_svn/threaded/commit.py | 2 ++ scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts-blender/addons/blender_svn/operators/simple_commands.py b/scripts-blender/addons/blender_svn/operators/simple_commands.py index 4f504077..7b5c0281 100644 --- a/scripts-blender/addons/blender_svn/operators/simple_commands.py +++ b/scripts-blender/addons/blender_svn/operators/simple_commands.py @@ -267,7 +267,7 @@ class SVN_OT_revert_and_update(SVN_OT_download_file_revision, SVN_OT_revert_file """Convenience operator for the "This file is outdated" warning message. Normally, these two operations should be done separately!""" bl_idname = "svn.revert_and_update_file" bl_label = "Revert And Update File" - bl_description = "The currently opened .blend file has a newer version available on the remote repository. Click to PERMANENTLY DISCARD local changes to this file and update it to the latest revision. Cannot be undone" + bl_description = "A different version of this file was downloaded while it was open. This warning will persist until the file is updated and reloaded, or committed. Click to PERMANENTLY DISCARD local changes to this file and update it to the latest revision. Cannot be undone" bl_options = {'INTERNAL'} missing_file_allowed = False diff --git a/scripts-blender/addons/blender_svn/operators/svn_commit.py b/scripts-blender/addons/blender_svn/operators/svn_commit.py index 0ec60eec..0ddace0c 100644 --- a/scripts-blender/addons/blender_svn/operators/svn_commit.py +++ b/scripts-blender/addons/blender_svn/operators/svn_commit.py @@ -155,7 +155,6 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator): def execute(self, context: Context) -> Set[str]: committable_files = self.get_committable_files(context) files_to_commit = [f for f in committable_files if f.include_in_commit] - prefs = get_addon_prefs(context) repo = context.scene.svn.get_repo(context) if not files_to_commit: diff --git a/scripts-blender/addons/blender_svn/threaded/commit.py b/scripts-blender/addons/blender_svn/threaded/commit.py index af7c7eb2..80ffeab5 100644 --- a/scripts-blender/addons/blender_svn/threaded/commit.py +++ b/scripts-blender/addons/blender_svn/threaded/commit.py @@ -51,6 +51,8 @@ class BGP_SVN_Commit(BackgroundProcess): print(self.output) repo = context.scene.svn.get_repo(context) for f in repo.external_files: + if f == repo.current_blend_file: + context.scene.file_is_outdated = False if f.status_prediction_type == 'SVN_COMMIT': f.status_prediction_type = 'SKIP_ONCE' Processes.start('Log') diff --git a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py index eef00392..a57b3cea 100644 --- a/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py +++ b/scripts-blender/addons/blender_svn/ui/ui_outdated_warning.py @@ -25,7 +25,7 @@ def draw_outdated_file_warning(self, context): row.operator('svn.resolve_conflict', text="SVN: This .blend file is conflicted.", icon='ERROR') elif current_file.repos_status != 'none' or context.scene.svn.file_is_outdated: - op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file is outdated.", icon='ERROR') + op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file may be outdated.", icon='ERROR') op.file_rel_path = repo.current_blend_file.svn_path -- 2.30.2