SVN: Checkout, Multi-Repo, Optimizations & Clean-up #104

Merged
Demeter Dzadik merged 12 commits from Mets/blender-studio-pipeline:SVN-improvements into main 2023-07-10 16:49:03 +02:00
27 changed files with 217 additions and 151 deletions
Showing only changes of commit b3e9c83ebe - Show all commits

View File

@ -2,6 +2,17 @@
# (c) 2021, Blender Foundation - Paul Golter # (c) 2021, Blender Foundation - Paul Golter
# (c) 2022, Blender Foundation - Demeter Dzadik # (c) 2022, Blender Foundation - Demeter Dzadik
from . import (
props,
repository,
operators,
threaded,
ui,
prefs,
svn_info,
)
import importlib
from bpy.utils import register_class, unregister_class
bl_info = { bl_info = {
"name": "Blender SVN", "name": "Blender SVN",
"author": "Demeter Dzadik, Paul Golter", "author": "Demeter Dzadik, Paul Golter",
@ -15,18 +26,6 @@ bl_info = {
"category": "Generic", "category": "Generic",
} }
from bpy.utils import register_class, unregister_class
import importlib
from . import (
props,
repository,
operators,
threaded,
ui,
prefs,
svn_info,
)
modules = [ modules = [
props, props,
@ -38,6 +37,7 @@ modules = [
svn_info, svn_info,
] ]
def register_unregister_modules(modules: list, register: bool): def register_unregister_modules(modules: list, register: bool):
"""Recursively register or unregister modules by looking for either """Recursively register or unregister modules by looking for either
un/register() functions or lists named `registry` which should be a list of un/register() functions or lists named `registry` which should be a list of
@ -54,7 +54,8 @@ def register_unregister_modules(modules: list, register: bool):
register_func(c) register_func(c)
except Exception as e: except Exception as e:
un = 'un' if not register else '' un = 'un' if not register else ''
print(f"Warning: SVN failed to {un}register class: {c.__name__}") print(
f"Warning: SVN failed to {un}register class: {c.__name__}")
print(e) print(e)
if hasattr(m, 'modules'): if hasattr(m, 'modules'):
@ -65,8 +66,10 @@ def register_unregister_modules(modules: list, register: bool):
elif hasattr(m, 'unregister'): elif hasattr(m, 'unregister'):
m.unregister() m.unregister()
def register(): def register():
register_unregister_modules(modules, True) register_unregister_modules(modules, True)
def unregister(): def unregister():
register_unregister_modules(modules, False) register_unregister_modules(modules, False)

View File

@ -41,7 +41,8 @@ class SVN_Operator_Single_File(SVN_Operator):
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
if not self.file_exists(context) and not type(self).missing_file_allowed: if not self.file_exists(context) and not type(self).missing_file_allowed:
# If the operator requires the file to exist and it doesn't, cancel. # If the operator requires the file to exist and it doesn't, cancel.
self.report({'ERROR'}, f'File is no longer on the file system: "{self.file_rel_path}"') self.report(
{'ERROR'}, f'File is no longer on the file system: "{self.file_rel_path}"')
return {'CANCELLED'} return {'CANCELLED'}
status = Processes.get('Status') status = Processes.get('Status')
@ -145,7 +146,8 @@ class SVN_OT_update_single(May_Modifiy_Current_Blend, Operator):
def _execute(self, context: Context) -> Set[str]: def _execute(self, context: Context) -> Set[str]:
self.will_conflict = False self.will_conflict = False
file_entry = context.scene.svn.get_repo(context).get_file_by_svn_path(self.file_rel_path) file_entry = context.scene.svn.get_repo(
context).get_file_by_svn_path(self.file_rel_path)
if file_entry.status not in ['normal', 'none']: if file_entry.status not in ['normal', 'none']:
self.will_conflict = True self.will_conflict = True
@ -179,7 +181,8 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
revision: IntProperty() revision: IntProperty()
def invoke(self, context, event): def invoke(self, context, event):
file_entry = context.scene.svn.get_repo(context).get_file_by_svn_path(self.file_rel_path) file_entry = context.scene.svn.get_repo(
context).get_file_by_svn_path(self.file_rel_path)
if self.file_is_current_blend(context) and file_entry.status != 'normal': if self.file_is_current_blend(context) and file_entry.status != 'normal':
self.report({'ERROR'}, self.report({'ERROR'},
'You must first revert or commit the changes to this file.') 'You must first revert or commit the changes to this file.')
@ -187,7 +190,8 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
return super().invoke(context, event) return super().invoke(context, event)
def _execute(self, context: Context) -> Set[str]: def _execute(self, context: Context) -> Set[str]:
file_entry = context.scene.svn.get_repo(context).get_file_by_svn_path(self.file_rel_path) file_entry = context.scene.svn.get_repo(
context).get_file_by_svn_path(self.file_rel_path)
if file_entry.status == 'modified': if file_entry.status == 'modified':
# If file has local modifications, let's avoid a conflict by cancelling # If file has local modifications, let's avoid a conflict by cancelling
# and telling the user to resolve it in advance. # and telling the user to resolve it in advance.
@ -197,7 +201,8 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
self.execute_svn_command( self.execute_svn_command(
context, context,
["svn", "up" ,f"-r{self.revision}", f"{self.file_rel_path}", "--accept", "postpone"], ["svn", "up", f"-r{self.revision}",
f"{self.file_rel_path}", "--accept", "postpone"],
use_cred=True use_cred=True
) )
@ -227,7 +232,7 @@ class SVN_OT_restore_file(May_Modifiy_Current_Blend, Operator):
def _execute(self, context: Context) -> Set[str]: def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command( self.execute_svn_command(
context, context,
["svn", "revert", f"{self.file_rel_path}"] ["svn", "revert", f"{self.file_rel_path}"]
) )
@ -327,7 +332,7 @@ class SVN_OT_remove_file(SVN_Operator_Single_File, Warning_Operator, Operator):
def _execute(self, context: Context) -> Set[str]: def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command( self.execute_svn_command(
context, context,
["svn", "remove", f"{self.file_rel_path}"] ["svn", "remove", f"{self.file_rel_path}"]
) )
@ -375,7 +380,8 @@ class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator):
def _execute(self, context: Context) -> Set[str]: def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command( self.execute_svn_command(
context, context,
["svn", "resolve", f"{self.file_rel_path}", "--accept", f"{self.resolve_method}"] ["svn", "resolve", f"{self.file_rel_path}",
"--accept", f"{self.resolve_method}"]
) )
return {"FINISHED"} return {"FINISHED"}
@ -400,7 +406,7 @@ class SVN_OT_cleanup(SVN_Operator, Operator):
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
repo = context.scene.svn.get_repo(context) repo = context.scene.svn.get_repo(context)
repo.external_files.clear() repo.external_files.clear()
self.execute_svn_command(context, ["svn", "cleanup"]) self.execute_svn_command(context, ["svn", "cleanup"])
repo.reload_svn_log(context) repo.reload_svn_log(context)

View File

@ -13,6 +13,7 @@ from ..threaded.background_process import Processes
import subprocess import subprocess
from pathlib import Path from pathlib import Path
class SVN_OT_checkout_initiate(Operator): class SVN_OT_checkout_initiate(Operator):
bl_idname = "svn.checkout_initiate" bl_idname = "svn.checkout_initiate"
bl_label = "Initiate SVN Checkout" bl_label = "Initiate SVN Checkout"
@ -20,9 +21,9 @@ class SVN_OT_checkout_initiate(Operator):
bl_options = {'INTERNAL'} bl_options = {'INTERNAL'}
create: BoolProperty( create: BoolProperty(
name = "Create Repo Entry", name="Create Repo Entry",
description = "Whether a new repo entry should be created, or the active one used", description="Whether a new repo entry should be created, or the active one used",
default = True default=True
) )
def execute(self, context): def execute(self, context):
@ -35,6 +36,7 @@ class SVN_OT_checkout_initiate(Operator):
prefs.checkout_mode = True prefs.checkout_mode = True
return {'FINISHED'} return {'FINISHED'}
class SVN_OT_checkout_finalize(Operator, SVN_Operator): class SVN_OT_checkout_finalize(Operator, SVN_Operator):
bl_idname = "svn.checkout_finalize" bl_idname = "svn.checkout_finalize"
bl_label = "Finalize SVN Checkout" bl_label = "Finalize SVN Checkout"
@ -54,11 +56,12 @@ class SVN_OT_checkout_finalize(Operator, SVN_Operator):
['svn', 'cleanup'] ['svn', 'cleanup']
) )
p = subprocess.Popen( p = subprocess.Popen(
["svn", "checkout", f"--username={repo.username}", f"--password={repo.password}", repo.url, repo.display_name], ["svn", "checkout", f"--username={repo.username}",
shell = False, f"--password={repo.password}", repo.url, repo.display_name],
cwd = repo.directory+"/", shell=False,
stdout = subprocess.PIPE, cwd=repo.directory+"/",
start_new_session = True stdout=subprocess.PIPE,
start_new_session=True
) )
repo.directory = str((Path(repo.directory) / repo.display_name)) repo.directory = str((Path(repo.directory) / repo.display_name))
while True: while True:
@ -87,6 +90,7 @@ class SVN_OT_checkout_cancel(Operator):
prefs.repositories.remove(prefs.active_repo_idx) prefs.repositories.remove(prefs.active_repo_idx)
return {'FINISHED'} return {'FINISHED'}
registry = [ registry = [
SVN_OT_checkout_initiate, SVN_OT_checkout_initiate,
SVN_OT_checkout_finalize, SVN_OT_checkout_finalize,

View File

@ -86,7 +86,7 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
self.is_file_really_dirty = bpy.data.is_dirty self.is_file_really_dirty = bpy.data.is_dirty
# This flag is needed as a workaround because bpy.data.is_dirty gets set to True # This flag is needed as a workaround because bpy.data.is_dirty gets set to True
# when we change the operator's checkboxes or # when we change the operator's checkboxes or
self.is_file_dirty_on_invoke = bpy.data.is_dirty self.is_file_dirty_on_invoke = bpy.data.is_dirty
for f in repo.external_files: for f in repo.external_files:
@ -119,7 +119,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
icon = 'ERROR' icon = 'ERROR'
op_row = split.row() op_row = split.row()
op_row.alignment = 'LEFT' op_row.alignment = 'LEFT'
op_row.operator('svn.save_during_commit', icon='FILE_BLEND', text="Save") op_row.operator('svn.save_during_commit',
icon='FILE_BLEND', text="Save")
row.label(text=text, icon=icon) row.label(text=text, icon=icon)
row = layout.row() row = layout.row()
@ -159,9 +160,9 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
self.set_predicted_file_statuses(files_to_commit) self.set_predicted_file_statuses(files_to_commit)
Processes.stop('Status') Processes.stop('Status')
Processes.start('Commit', Processes.start('Commit',
commit_msg=repo.commit_message, commit_msg=repo.commit_message,
file_list=filepaths file_list=filepaths
) )
report = f"{(len(files_to_commit))} files" report = f"{(len(files_to_commit))} files"
if len(files_to_commit) == 1: if len(files_to_commit) == 1:

View File

@ -19,9 +19,9 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
bl_options = {'INTERNAL'} bl_options = {'INTERNAL'}
revision: IntProperty( revision: IntProperty(
name = "Revision", name="Revision",
description = "Which revision to revert the repository to. 0 means to update to the latest version instead", description="Which revision to revert the repository to. 0 means to update to the latest version instead",
default = 0 default=0
) )
@classmethod @classmethod
@ -50,7 +50,7 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
for f in repo.external_files: for f in repo.external_files:
if f.status in ['modified', 'added', 'conflicted', 'deleted', 'missing', 'unversioned']: if f.status in ['modified', 'added', 'conflicted', 'deleted', 'missing', 'unversioned']:
return context.window_manager.invoke_props_dialog(self, width=500) return context.window_manager.invoke_props_dialog(self, width=500)
return self.execute(context) return self.execute(context)
def draw(self, context): def draw(self, context):
@ -58,10 +58,13 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
layout = self.layout layout = self.layout
col = layout.column() col = layout.column()
col.label(text="You have uncommitted local changes.") col.label(text="You have uncommitted local changes.")
col.label(text="These won't be lost, but if you want to revert the state of the entire local repository to a ") col.label(
col.label(text="past point in time, you would get a better result if you reverted or committed your changes first.") text="These won't be lost, but if you want to revert the state of the entire local repository to a ")
col.label(
text="past point in time, you would get a better result if you reverted or committed your changes first.")
col.separator() col.separator()
col.label(text="Press OK to proceed anyways. Click out of this window to cancel.") col.label(
text="Press OK to proceed anyways. Click out of this window to cancel.")
super().draw(context) super().draw(context)
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
@ -72,7 +75,7 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
if self.revision > 0: if self.revision > 0:
command.insert(2, f"-r{self.revision}") command.insert(2, f"-r{self.revision}")
self.execute_svn_command( self.execute_svn_command(
context, context,
command, command,
use_cred=True use_cred=True
) )

View File

@ -3,6 +3,7 @@ from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator from bpy.types import Operator
from ..threaded.background_process import Processes from ..threaded.background_process import Processes
class SVN_OT_custom_tooltip(Operator): class SVN_OT_custom_tooltip(Operator):
"""Tooltip""" """Tooltip"""
bl_idname = "svn.custom_tooltip" bl_idname = "svn.custom_tooltip"
@ -70,4 +71,4 @@ class SVN_OT_clear_error(Operator):
registry = [ registry = [
SVN_OT_custom_tooltip, SVN_OT_custom_tooltip,
SVN_OT_clear_error SVN_OT_clear_error
] ]

View File

@ -15,6 +15,7 @@ import json
from pathlib import Path from pathlib import Path
from .threaded.background_process import Processes from .threaded.background_process import Processes
class SVN_addon_preferences(AddonPreferences): class SVN_addon_preferences(AddonPreferences):
bl_idname = __package__ bl_idname = __package__
@ -56,7 +57,7 @@ class SVN_addon_preferences(AddonPreferences):
self.active_repo_idx = scene_svn_idx self.active_repo_idx = scene_svn_idx
self.idx_updating = False self.idx_updating = False
return return
if not active_repo.authenticated and not active_repo.auth_failed and active_repo.is_cred_entered: if not active_repo.authenticated and not active_repo.auth_failed and active_repo.is_cred_entered:
active_repo.authenticate(context) active_repo.authenticate(context)
@ -75,14 +76,15 @@ class SVN_addon_preferences(AddonPreferences):
) )
active_repo_mode: EnumProperty( active_repo_mode: EnumProperty(
name = "Choose Repository", 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", 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 = [ 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"), ('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") ('SELECTED_REPO', "Selected Repo",
"Communicate with the selected repository")
], ],
default = 'CURRENT_BLEND', default='CURRENT_BLEND',
update = update_active_repo_mode update=update_active_repo_mode
) )
active_repo_idx: IntProperty( active_repo_idx: IntProperty(
@ -101,11 +103,11 @@ class SVN_addon_preferences(AddonPreferences):
return self.repositories[self.active_repo_idx] return self.repositories[self.active_repo_idx]
debug_mode: BoolProperty( debug_mode: BoolProperty(
name = "Debug Mode", name="Debug Mode",
description = "Enable some debug UI", description="Enable some debug UI",
default = False default=False
) )
@property @property
def is_busy(self): def is_busy(self):
return Processes.is_running('Commit', 'Update') return Processes.is_running('Commit', 'Update')
@ -117,21 +119,25 @@ class SVN_addon_preferences(AddonPreferences):
) )
def save_repo_info_to_file(self): def save_repo_info_to_file(self):
saved_props = {'url', 'directory', 'name', 'username', 'password', 'display_name'} saved_props = {'url', 'directory', 'name',
'username', 'password', 'display_name'}
repo_data = {} repo_data = {}
for repo in self['repositories']: for repo in self['repositories']:
directory = repo.get('directory', '') directory = repo.get('directory', '')
repo_data[directory] = {key:value for key, value in repo.to_dict().items() if key in saved_props} repo_data[directory] = {
key: value for key, value in repo.to_dict().items() if key in saved_props}
filepath = Path(bpy.utils.user_resource('CONFIG')) / Path("blender_svn.txt") filepath = Path(bpy.utils.user_resource('CONFIG')) / \
Path("blender_svn.txt")
with open(filepath, "w") as f: with open(filepath, "w") as f:
json.dump(repo_data, f, indent=4) json.dump(repo_data, f, indent=4)
def load_repo_info_from_file(self): def load_repo_info_from_file(self):
self.loading = True self.loading = True
try: try:
filepath = Path(bpy.utils.user_resource('CONFIG')) / Path("blender_svn.txt") filepath = Path(bpy.utils.user_resource(
'CONFIG')) / Path("blender_svn.txt")
if not filepath.exists(): if not filepath.exists():
return return
@ -154,6 +160,7 @@ class SVN_addon_preferences(AddonPreferences):
draw = ui_prefs.draw_prefs draw = ui_prefs.draw_prefs
registry = [ registry = [
SVN_addon_preferences SVN_addon_preferences
] ]

View File

@ -2,18 +2,16 @@
# (c) 2021, Blender Foundation - Paul Golter # (c) 2021, Blender Foundation - Paul Golter
# (c) 2022, Blender Foundation - Demeter Dzadik # (c) 2022, Blender Foundation - Demeter Dzadik
from .util import get_addon_prefs
from bpy.props import StringProperty, PointerProperty
from bpy.types import PropertyGroup
import bpy
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple, Set
from . import wheels from . import wheels
# This will load the dateutil and BAT wheel files. # This will load the dateutil and BAT wheel files.
wheels.preload_dependencies() wheels.preload_dependencies()
from typing import Optional, Dict, Any, List, Tuple, Set
from pathlib import Path
import bpy
from bpy.types import PropertyGroup
from bpy.props import StringProperty, PointerProperty
from .util import get_addon_prefs
class SVN_scene_properties(PropertyGroup): class SVN_scene_properties(PropertyGroup):
"""Subversion properties to match this scene to a repo in the UserPrefs""" """Subversion properties to match this scene to a repo in the UserPrefs"""

View File

@ -14,6 +14,7 @@ from .svn_info import get_svn_info
from .util import get_addon_prefs from .util import get_addon_prefs
from . import constants from . import constants
class SVN_file(PropertyGroup): class SVN_file(PropertyGroup):
"""Property Group that can represent a version of a File in an SVN repository.""" """Property Group that can represent a version of a File in an SVN repository."""
@ -52,7 +53,8 @@ class SVN_file(PropertyGroup):
items=[ items=[
("NONE", "None", "File status is not predicted, but actual."), ("NONE", "None", "File status is not predicted, but actual."),
("SVN_UP", "Update", "File status is predicted by `svn up`. Status is protected until process is finished."), ("SVN_UP", "Update", "File status is predicted by `svn up`. Status is protected until process is finished."),
("SVN_COMMIT", "Commit", "File status is predicted by `svn commit`. Status is protected until process is finished."), ("SVN_COMMIT", "Commit",
"File status is predicted by `svn commit`. Status is protected until process is finished."),
("SKIP_ONCE", "Skip Once", "File status is predicted by a working-copy svn file operation, like Revert. Next status update should be ignored, and this enum should be set to SKIPPED_ONCE."), ("SKIP_ONCE", "Skip Once", "File status is predicted by a working-copy svn file operation, like Revert. Next status update should be ignored, and this enum should be set to SKIPPED_ONCE."),
("SKIPPED_ONCE", "Skipped Once", "File status update was skipped. Next status update can be considered accurate, and this flag can be reset to NONE. Until then, operations on this file should remain disabled."), ("SKIPPED_ONCE", "Skipped Once", "File status update was skipped. Next status update can be considered accurate, and this flag can be reset to NONE. Until then, operations on this file should remain disabled."),
], ],
@ -164,6 +166,7 @@ class SVN_log(PropertyGroup):
name="Changed Files", name="Changed Files",
description="List of file entries that were affected by this revision" description="List of file entries that were affected by this revision"
) )
def changes_file(self, file: SVN_file) -> bool: def changes_file(self, file: SVN_file) -> bool:
for affected_file in self.changed_files: for affected_file in self.changed_files:
if affected_file.svn_path == "/"+file.svn_path: if affected_file.svn_path == "/"+file.svn_path:
@ -190,7 +193,7 @@ class SVN_log(PropertyGroup):
rev = "r"+str(self.revision_number) rev = "r"+str(self.revision_number)
auth = self.revision_author auth = self.revision_author
files = " ".join([f.svn_path for f in self.changed_files]) files = " ".join([f.svn_path for f in self.changed_files])
msg = self.commit_message msg = self.commit_message
date = self.revision_date_simple date = self.revision_date_simple
return " ".join([rev, auth, files, msg, date]).lower() return " ".join([rev, auth, files, msg, date]).lower()
@ -200,6 +203,7 @@ class SVN_log(PropertyGroup):
default=False default=False
) )
class SVN_repository(PropertyGroup): class SVN_repository(PropertyGroup):
### Basic SVN Info. ### ### Basic SVN Info. ###
@property @property
@ -210,14 +214,14 @@ class SVN_repository(PropertyGroup):
get_addon_prefs(context).save_repo_info_to_file() get_addon_prefs(context).save_repo_info_to_file()
display_name: StringProperty( display_name: StringProperty(
name = "Display Name", name="Display Name",
description = "Display name of this SVN repository", description="Display name of this SVN repository",
update = update_repo_info_file update=update_repo_info_file
) )
url: StringProperty( url: StringProperty(
name = "URL", name="URL",
description = "URL of the remote repository", description="URL of the remote repository",
) )
def update_directory(self, context): def update_directory(self, context):
@ -245,8 +249,8 @@ class SVN_repository(PropertyGroup):
dir_path = Path(self.directory) dir_path = Path(self.directory)
root_dir, base_url = get_svn_info(self.directory) root_dir, base_url = get_svn_info(self.directory)
return ( return (
dir_path.exists() and dir_path.exists() and
dir_path.is_dir() and dir_path.is_dir() and
root_dir and base_url and root_dir and base_url and
root_dir == self.directory and root_dir == self.directory and
base_url == self.url base_url == self.url
@ -385,7 +389,6 @@ class SVN_repository(PropertyGroup):
current = file.revision current = file.revision
return latest > current return latest > current
### SVN File List. ### ### SVN File List. ###
external_files: CollectionProperty(type=SVN_file) external_files: CollectionProperty(type=SVN_file)
@ -461,8 +464,9 @@ class SVN_repository(PropertyGroup):
# Filter out log entries that did not affect the selected file. # Filter out log entries that did not affect the selected file.
self.log.foreach_set( self.log.foreach_set(
'affects_active_file', 'affects_active_file',
[log_entry.changes_file(self.active_file) for log_entry in self.log] [log_entry.changes_file(self.active_file)
for log_entry in self.log]
) )
external_files_active_index: IntProperty( external_files_active_index: IntProperty(
@ -509,8 +513,8 @@ class SVN_repository(PropertyGroup):
return self.get_file_by_absolute_path(bpy.data.filepath) return self.get_file_by_absolute_path(bpy.data.filepath)
### File List UIList filter properties ### ### File List UIList filter properties ###
# Filtering properties are normally stored on the UIList, # Filtering properties are normally stored on the UIList,
# but then they cannot be accessed from anywhere else, # but then they cannot be accessed from anywhere else,
# since template_list() does not return the UIList instance. # 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 # 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. # ensure that a filtered out entry can never be the active one.
@ -531,13 +535,13 @@ class SVN_repository(PropertyGroup):
def update_file_filter(self, context): def update_file_filter(self, context):
"""Should run when any of the SVN file list search filters are changed.""" """Should run when any of the SVN file list search filters are changed."""
UI_LIST = bpy.types.UI_UL_list UI_LIST = bpy.types.UI_UL_list
if self.file_search_filter: if self.file_search_filter:
filter_list = UI_LIST.filter_items_by_name( filter_list = UI_LIST.filter_items_by_name(
self.file_search_filter, self.file_search_filter,
1, 1,
self.external_files, self.external_files,
"name", "name",
reverse=False reverse=False
) )
@ -564,4 +568,4 @@ registry = [
SVN_file, SVN_file,
SVN_log, SVN_log,
SVN_repository, SVN_repository,
] ]

View File

@ -4,6 +4,7 @@ import subprocess
from .threaded.execute_subprocess import execute_command from .threaded.execute_subprocess import execute_command
def get_svn_info(path: Path or str) -> Tuple[str, str]: def get_svn_info(path: Path or str) -> Tuple[str, str]:
"""Use the `svn info` command to get the root dir, the URL, and the relative URL.""" """Use the `svn info` command to get the root dir, the URL, and the relative URL."""
path = Path(path) path = Path(path)

View File

@ -2,7 +2,8 @@
# (c) 2022, Blender Foundation - Demeter Dzadik # (c) 2022, Blender Foundation - Demeter Dzadik
import bpy import bpy
import threading, subprocess import threading
import subprocess
import random import random
from typing import List from typing import List
@ -180,8 +181,8 @@ class BackgroundProcess:
if not bpy.app.timers.is_registered(self.timer_function): if not bpy.app.timers.is_registered(self.timer_function):
self.debug_print("Register timer") self.debug_print("Register timer")
bpy.app.timers.register( bpy.app.timers.register(
self.timer_function, self.timer_function,
first_interval=self.first_interval, first_interval=self.first_interval,
persistent=persistent persistent=persistent
) )
@ -206,6 +207,8 @@ def get_recursive_subclasses(typ) -> List[type]:
processes = {} processes = {}
class ProcessManager: class ProcessManager:
@property @property
def processes(self): def processes(self):
@ -227,7 +230,6 @@ class ProcessManager:
if proc_name in self.processes: if proc_name in self.processes:
return self.processes[proc_name].is_running return self.processes[proc_name].is_running
def get(self, proc_name: str): def get(self, proc_name: str):
return self.processes.get(proc_name) return self.processes.get(proc_name)
@ -259,6 +261,7 @@ class ProcessManager:
process.stop() process.stop()
del self.processes[proc_name] del self.processes[proc_name]
# I named this variable with title-case, to indicate that it's a Singleton. # I named this variable with title-case, to indicate that it's a Singleton.
# There should only be one. # There should only be one.
Processes = ProcessManager() Processes = ProcessManager()

View File

@ -10,6 +10,7 @@ from .background_process import Processes, BackgroundProcess
from .execute_subprocess import execute_svn_command from .execute_subprocess import execute_svn_command
from ..util import get_addon_prefs from ..util import get_addon_prefs
class BGP_SVN_Commit(BackgroundProcess): class BGP_SVN_Commit(BackgroundProcess):
name = "Commit" name = "Commit"
needs_authentication = True needs_authentication = True
@ -33,7 +34,8 @@ class BGP_SVN_Commit(BackgroundProcess):
Processes.kill('Status') Processes.kill('Status')
sanitized_commit_msg = self.commit_msg.replace('"', "'") sanitized_commit_msg = self.commit_msg.replace('"', "'")
command = ["svn", "commit", "-m", f"{sanitized_commit_msg}"] + self.file_list command = ["svn", "commit", "-m",
f"{sanitized_commit_msg}"] + self.file_list
self.output = execute_svn_command( self.output = execute_svn_command(
context, context,
command, command,
@ -67,4 +69,3 @@ class BGP_SVN_Commit(BackgroundProcess):
def stop(self): def stop(self):
super().stop() super().stop()

View File

@ -4,6 +4,7 @@
import subprocess import subprocess
from typing import List from typing import List
def get_credential_commands(context) -> List[str]: def get_credential_commands(context) -> List[str]:
repo = context.scene.svn.get_repo(context) 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!" 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!"

View File

@ -1,5 +1,6 @@
from .background_process import BackgroundProcess from .background_process import BackgroundProcess
class BGP_SVN_Activate_File(BackgroundProcess): class BGP_SVN_Activate_File(BackgroundProcess):
"""This crazy hacky method of activating the file with some delay is necessary """This crazy hacky method of activating the file with some delay is necessary
because Blender won't let us select the file immediately when changing the because Blender won't let us select the file immediately when changing the

View File

@ -1,6 +1,7 @@
from .background_process import BackgroundProcess, Processes from .background_process import BackgroundProcess, Processes
from ..util import redraw_viewport from ..util import redraw_viewport
class BGP_SVN_Redraw_Viewport(BackgroundProcess): class BGP_SVN_Redraw_Viewport(BackgroundProcess):
name = "Redraw Viewport" name = "Redraw Viewport"
repeat_delay = 1 repeat_delay = 1
@ -18,4 +19,4 @@ class BGP_SVN_Redraw_Viewport(BackgroundProcess):
def register(): def register():
Processes.start("Redraw Viewport") Processes.start("Redraw Viewport")

View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# (c) 2022, Blender Foundation - Demeter Dzadik # (c) 2022, Blender Foundation - Demeter Dzadik
from datetime import datetime
from pathlib import Path from pathlib import Path
import subprocess import subprocess
@ -57,7 +58,8 @@ def reload_svn_log(self, context):
log_entry.revision_author = r_author log_entry.revision_author = r_author
log_entry.revision_date = r_date log_entry.revision_date = r_date
log_entry.revision_date_simple = svn_date_simple(r_date).split(" ")[0][5:] log_entry.revision_date_simple = svn_date_simple(r_date).split(" ")[
0][5:]
# File change set is on line 3 until the commit message begins... # File change set is on line 3 until the commit message begins...
file_change_lines = chunk[2:-(r_msg_length+1)] file_change_lines = chunk[2:-(r_msg_length+1)]
@ -74,7 +76,8 @@ def reload_svn_log(self, context):
log_file_entry = log_entry.changed_files.add() log_file_entry = log_entry.changed_files.add()
log_file_entry.name = file_path.name log_file_entry.name = file_path.name
log_file_entry.svn_path = str(file_path.as_posix()) log_file_entry.svn_path = str(file_path.as_posix())
log_file_entry.absolute_path = str(repo.svn_to_absolute_path(file_path).as_posix()) log_file_entry.absolute_path = str(
repo.svn_to_absolute_path(file_path).as_posix())
log_file_entry.revision = r_number log_file_entry.revision = r_number
log_file_entry.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char] log_file_entry.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char]
@ -146,7 +149,8 @@ class BGP_SVN_Log(BackgroundProcess):
try: try:
self.output = execute_svn_command( self.output = execute_svn_command(
context, context,
["svn", "log", "--verbose", f"-r{latest_log_rev+1}:HEAD", "--limit", "10"], ["svn", "log", "--verbose",
f"-r{latest_log_rev+1}:HEAD", "--limit", "10"],
print_errors=False, print_errors=False,
use_cred=True use_cred=True
) )
@ -170,7 +174,7 @@ class BGP_SVN_Log(BackgroundProcess):
rev_no = repo.log[-1].revision_number rev_no = repo.log[-1].revision_number
return f"Updating log. Current: r{rev_no}..." return f"Updating log. Current: r{rev_no}..."
from datetime import datetime
def svn_date_to_datetime(datetime_str: str) -> datetime: def svn_date_to_datetime(datetime_str: str) -> datetime:
"""Convert a string from SVN's datetime format to a datetime object.""" """Convert a string from SVN's datetime format to a datetime object."""
date, time, _timezone, _day, _n_day, _mo, _y = datetime_str.split(" ") date, time, _timezone, _day, _n_day, _mo, _y = datetime_str.split(" ")
@ -184,4 +188,4 @@ def svn_date_simple(datetime_str: str) -> str:
date_str = f"{dt.year}-{month_name}-{dt.day}" date_str = f"{dt.year}-{month_name}-{dt.day}"
time_str = f"{str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}" time_str = f"{str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
return date_str + " " + time_str return date_str + " " + time_str

View File

@ -1,25 +1,22 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# (c) 2022, Blender Foundation - Demeter Dzadik # (c) 2022, Blender Foundation - Demeter Dzadik
from ..svn_info import get_svn_info
from ..util import get_addon_prefs
from .. import constants
from .execute_subprocess import execute_svn_command
from .background_process import BackgroundProcess, Processes
from bpy.types import Operator
from bpy.props import StringProperty
import bpy
import xmltodict
import time
from pathlib import Path
from typing import List, Dict, Union, Any, Set, Optional, Tuple
from .. import wheels from .. import wheels
# This will load the xmltodict wheel file. # This will load the xmltodict wheel file.
wheels.preload_dependencies() wheels.preload_dependencies()
from typing import List, Dict, Union, Any, Set, Optional, Tuple
from pathlib import Path
import time
import xmltodict
import bpy
from bpy.props import StringProperty
from bpy.types import Operator
from .background_process import BackgroundProcess, Processes
from .execute_subprocess import execute_svn_command
from .. import constants
from ..util import get_addon_prefs
from ..svn_info import get_svn_info
class SVN_OT_explain_status(Operator): class SVN_OT_explain_status(Operator):
bl_idname = "svn.explain_status" bl_idname = "svn.explain_status"
@ -72,7 +69,7 @@ def init_svn_of_current_file(_scene=None):
prefs.sync_repo_info_file() prefs.sync_repo_info_file()
for repo in prefs.repositories: for repo in prefs.repositories:
# This would ideally only run when opening Blender for the first # This would ideally only run when opening Blender for the first
# time, but there is no app handler for that, sadly. # time, but there is no app handler for that, sadly.
repo.authenticated = False repo.authenticated = False
repo.auth_failed = False repo.auth_failed = False
@ -89,7 +86,7 @@ def init_svn_of_current_file(_scene=None):
repo = scene_svn.get_scene_repo(context) repo = scene_svn.get_scene_repo(context)
if not repo: if not repo:
repo = prefs.init_repo(context, scene_svn.svn_directory) repo = prefs.init_repo(context, scene_svn.svn_directory)
for i, other_repo in enumerate(prefs.repositories): for i, other_repo in enumerate(prefs.repositories):
if other_repo == repo: if other_repo == repo:
prefs.active_repo_idx = i prefs.active_repo_idx = i
@ -129,6 +126,7 @@ def set_scene_svn_info(context) -> bool:
############## AUTOMATICALLY KEEPING FILE STATUSES UP TO DATE ################## ############## AUTOMATICALLY KEEPING FILE STATUSES UP TO DATE ##################
################################################################################ ################################################################################
class BGP_SVN_Status(BackgroundProcess): class BGP_SVN_Status(BackgroundProcess):
name = "Status" name = "Status"
needs_authentication = True needs_authentication = True
@ -142,7 +140,7 @@ class BGP_SVN_Status(BackgroundProcess):
def acquire_output(self, context, prefs): def acquire_output(self, context, prefs):
self.output = execute_svn_command( self.output = execute_svn_command(
context, context,
["svn", "status", "--show-updates", "--verbose", "--xml"], ["svn", "status", "--show-updates", "--verbose", "--xml"],
use_cred=True use_cred=True
) )
@ -228,7 +226,8 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]):
entry_existed = False entry_existed = False
file_entry = repo.external_files.add() file_entry = repo.external_files.add()
file_entry.svn_path = svn_path_str file_entry.svn_path = svn_path_str
file_entry.absolute_path = str(repo.svn_to_absolute_path(svn_path).as_posix()) file_entry.absolute_path = str(
repo.svn_to_absolute_path(svn_path).as_posix())
file_entry['name'] = svn_path.name file_entry['name'] = svn_path.name
if not file_entry.exists: if not file_entry.exists:
@ -261,7 +260,8 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]):
# File entry status has changed between local and repo. # File entry status has changed between local and repo.
file_strings = [] file_strings = []
for svn_path, repos_status in new_files_on_repo: for svn_path, repos_status in new_files_on_repo:
status_char = constants.SVN_STATUS_NAME_TO_CHAR.get(repos_status, " ") status_char = constants.SVN_STATUS_NAME_TO_CHAR.get(
repos_status, " ")
file_strings.append(f"{status_char} {svn_path}") file_strings.append(f"{status_char} {svn_path}")
print( print(
"SVN: Detected file changes on remote:\n", "SVN: Detected file changes on remote:\n",

View File

@ -25,7 +25,7 @@ class BGP_SVN_Update(BackgroundProcess):
if self.revision > 0: if self.revision > 0:
command.insert(2, f"-r{self.revision}") command.insert(2, f"-r{self.revision}")
self.output = execute_svn_command( self.output = execute_svn_command(
context, context,
command, command,
use_cred=True use_cred=True
) )
@ -44,7 +44,6 @@ class BGP_SVN_Update(BackgroundProcess):
Processes.start('Log') Processes.start('Log')
Processes.start('Status') Processes.start('Status')
def get_ui_message(self, context) -> str: def get_ui_message(self, context) -> str:
"""Return a string that should be drawn in the UI for user feedback, """Return a string that should be drawn in the UI for user feedback,
depending on the state of the process.""" depending on the state of the process."""

View File

@ -16,4 +16,4 @@ modules = [
ui_prefs, ui_prefs,
ui_outdated_warning, ui_outdated_warning,
ui_context_menus ui_context_menus
] ]

View File

@ -6,6 +6,7 @@ from bpy.types import Context, UIList, Operator
from bpy.props import StringProperty from bpy.props import StringProperty
from pathlib import Path from pathlib import Path
class SVN_OT_open_blend_file(Operator): class SVN_OT_open_blend_file(Operator):
# This is needed because drawing a button for wm.open_mainfile in the UI # 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, # directly simply does not work; Blender just opens a full-screen filebrowser,
@ -18,7 +19,7 @@ class SVN_OT_open_blend_file(Operator):
filepath: StringProperty() filepath: StringProperty()
def execute(self, context): def execute(self, context):
bpy.ops.wm.open_mainfile(filepath=self.filepath, load_ui = False) bpy.ops.wm.open_mainfile(filepath=self.filepath, load_ui=False)
return {'FINISHED'} return {'FINISHED'}
@ -39,7 +40,7 @@ def svn_file_list_context_menu(self: UIList, context: Context) -> None:
active_file = context.scene.svn.get_repo(context).active_file active_file = context.scene.svn.get_repo(context).active_file
if active_file.name.endswith("blend"): if active_file.name.endswith("blend"):
layout.operator("svn.open_blend_file", layout.operator("svn.open_blend_file",
text=f"Open {active_file.name}").filepath = active_file.absolute_path text=f"Open {active_file.name}").filepath = active_file.absolute_path
else: else:
layout.operator("wm.path_open", layout.operator("wm.path_open",
text=f"Open {active_file.name}").filepath = str(Path(active_file.absolute_path)) text=f"Open {active_file.name}").filepath = str(Path(active_file.absolute_path))
@ -67,8 +68,10 @@ def register():
bpy.types.UI_MT_list_item_context_menu.append(svn_file_list_context_menu) bpy.types.UI_MT_list_item_context_menu.append(svn_file_list_context_menu)
bpy.types.UI_MT_list_item_context_menu.append(svn_log_list_context_menu) bpy.types.UI_MT_list_item_context_menu.append(svn_log_list_context_menu)
def unregister(): 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_file_list_context_menu)
bpy.types.UI_MT_list_item_context_menu.remove(svn_log_list_context_menu) bpy.types.UI_MT_list_item_context_menu.remove(svn_log_list_context_menu)
registry = [SVN_OT_open_blend_file]
registry = [SVN_OT_open_blend_file]

View File

@ -122,7 +122,8 @@ class SVN_UL_file_list(UIList):
element becomes hidden.""" element becomes hidden."""
flt_neworder = [] flt_neworder = []
list_items = getattr(data, propname) list_items = getattr(data, propname)
flt_flags = [file.show_in_filelist * cls.UILST_FLT_ITEM for file in list_items] flt_flags = [file.show_in_filelist *
cls.UILST_FLT_ITEM for file in list_items]
helper_funcs = bpy.types.UI_UL_list helper_funcs = bpy.types.UI_UL_list
@ -147,7 +148,8 @@ class SVN_UL_file_list(UIList):
row.prop(self, 'show_file_paths', text="", row.prop(self, 'show_file_paths', text="",
toggle=True, icon="FILE_FOLDER") toggle=True, icon="FILE_FOLDER")
row.prop(context.scene.svn.get_repo(context), 'file_search_filter', text="") row.prop(context.scene.svn.get_repo(context),
'file_search_filter', text="")
def draw_process_info(context, layout): def draw_process_info(context, layout):
@ -167,7 +169,7 @@ def draw_process_info(context, layout):
warning.process_id = process.name warning.process_id = process.name
any_error = True any_error = True
break break
if process.is_running: if process.is_running:
message = process.get_ui_message(context) message = process.get_ui_message(context)
if message: if message:
@ -177,7 +179,8 @@ def draw_process_info(context, layout):
if not any_error and process_message: if not any_error and process_message:
col.label(text=process_message) col.label(text=process_message)
if prefs.debug_mode: if prefs.debug_mode:
col.label(text="Processes: " + ", ".join([p.name for p in Processes.running_processes])) col.label(text="Processes: " +
", ".join([p.name for p in Processes.running_processes]))
def draw_repo_file_list(context, layout, repo): def draw_repo_file_list(context, layout, repo):
@ -223,7 +226,7 @@ def draw_repo_file_list(context, layout, repo):
col.separator() col.separator()
col.operator("svn.commit", icon='EXPORT', text="") col.operator("svn.commit", icon='EXPORT', text="")
col.operator("svn.update_all", icon='IMPORT', text="").revision=0 col.operator("svn.update_all", icon='IMPORT', text="").revision = 0
col.separator() col.separator()
col.operator("svn.cleanup", icon='BRUSH_DATA', text="") col.operator("svn.cleanup", icon='BRUSH_DATA', text="")

View File

@ -67,4 +67,3 @@ registry = [
FILEBROWSER_PT_SVN_files, FILEBROWSER_PT_SVN_files,
FILEBROWSER_PT_SVN_log FILEBROWSER_PT_SVN_log
] ]

View File

@ -62,14 +62,15 @@ class SVN_UL_log(UIList):
if not self.show_all_logs: if not self.show_all_logs:
flt_flags = [ flt_flags = [
log_entry.affects_active_file * self.bitflag_filter_item log_entry.affects_active_file * self.bitflag_filter_item
for log_entry in log_entries for log_entry in log_entries
] ]
if self.filter_name: if self.filter_name:
# Filtering: Allow comma-separated keywords. # Filtering: Allow comma-separated keywords.
# ALL keywords must be found somewhere in the log entry for it to show up. # ALL keywords must be found somewhere in the log entry for it to show up.
filter_words = [word.strip().lower() for word in self.filter_name.split(",")] filter_words = [word.strip().lower()
for word in self.filter_name.split(",")]
for idx, log_entry in enumerate(log_entries): for idx, log_entry in enumerate(log_entries):
for filter_word in filter_words: for filter_word in filter_words:
if filter_word not in log_entry.text_to_search: if filter_word not in log_entry.text_to_search:
@ -269,4 +270,4 @@ registry = [
SVN_UL_log, SVN_UL_log,
SVN_OT_log_tooltip, SVN_OT_log_tooltip,
SVN_OT_log_show_commit_msg SVN_OT_log_show_commit_msg
] ]

View File

@ -3,6 +3,7 @@
import bpy import bpy
def draw_outdated_file_warning(self, context): def draw_outdated_file_warning(self, context):
repo = context.scene.svn.get_repo(context) repo = context.scene.svn.get_repo(context)
if not repo: if not repo:
@ -31,6 +32,7 @@ def draw_outdated_file_warning(self, context):
'svn.custom_tooltip', text="SVN: This .blend file is outdated.", icon='ERROR') '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" 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"
def register(): def register():
bpy.types.VIEW3D_HT_header.prepend(draw_outdated_file_warning) bpy.types.VIEW3D_HT_header.prepend(draw_outdated_file_warning)

View File

@ -12,6 +12,7 @@ from .ui_file_list import draw_repo_file_list, draw_process_info
from ..threaded.background_process import Processes from ..threaded.background_process import Processes
import platform import platform
class SVN_UL_repositories(UIList): 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 repo = item
@ -26,6 +27,7 @@ class SVN_UL_repositories(UIList):
row.alert = True row.alert = True
row.prop(repo, 'directory', text="") row.prop(repo, 'directory', text="")
class SVN_OT_repo_add(Operator, ImportHelper): class SVN_OT_repo_add(Operator, ImportHelper):
"""Add a repository to the list""" """Add a repository to the list"""
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -45,7 +47,8 @@ class SVN_OT_repo_add(Operator, ImportHelper):
try: try:
repo = prefs.init_repo(context, path) repo = prefs.init_repo(context, path)
except Exception as e: 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.") self.report(
{'ERROR'}, "Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.")
print(e) print(e)
return {'CANCELLED'} return {'CANCELLED'}
if not repo: if not repo:
@ -59,6 +62,7 @@ class SVN_OT_repo_add(Operator, ImportHelper):
prefs.save_repo_info_to_file() prefs.save_repo_info_to_file()
return {'FINISHED'} return {'FINISHED'}
class SVN_OT_repo_remove(Operator): class SVN_OT_repo_remove(Operator):
"""Remove a repository from the list""" """Remove a repository from the list"""
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -81,14 +85,17 @@ class SVN_OT_repo_remove(Operator):
prefs.save_repo_info_to_file() prefs.save_repo_info_to_file()
return {'FINISHED'} return {'FINISHED'}
class SVN_MT_add_repo(Menu): class SVN_MT_add_repo(Menu):
bl_idname = "SVN_MT_add_repo" bl_idname = "SVN_MT_add_repo"
bl_label = "Add Repo" bl_label = "Add Repo"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator("svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER') layout.operator(
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): def draw_prefs(self, context):
@ -103,7 +110,7 @@ def draw_prefs_checkout(self, context):
msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console." 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_linux = "If you don't, quit Blender and re-launch it from a terminal."
msg_mac = msg_linux msg_mac = msg_linux
system = platform.system() system = platform.system()
if system == "Windows": if system == "Windows":
return msg_windows return msg_windows
@ -120,13 +127,17 @@ def draw_prefs_checkout(self, context):
col.label(text="Make sure you have Blender's terminal open!") col.label(text="Make sure you have Blender's terminal open!")
col.label(text=get_terminal_howto()) col.label(text=get_terminal_howto())
col.separator() col.separator()
col.label(text="Downloading a repository can take a long time, and the UI will be locked.") col.label(
col.label(text="Without a terminal, you won't be able to track the progress of the checkout.") 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.separator()
col = layout.column() col = layout.column()
col.label(text="To interrupt the checkout, you can press Ctrl+C in the terminal.", icon='INFO') col.label(
col.label(text="You can resume it by re-running this operation, or with the SVN Update button.", icon='INFO') 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() col.separator()
prefs = get_addon_prefs(context) prefs = get_addon_prefs(context)
@ -137,8 +148,9 @@ def draw_prefs_checkout(self, context):
continue continue
if other_repo.directory == repo.directory: if other_repo.directory == repo.directory:
row = col.row() row = col.row()
row.alert=True row.alert = True
row.label(text="A repository at this filepath is already specified.", icon='ERROR') row.label(
text="A repository at this filepath is already specified.", icon='ERROR')
break break
col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER') col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER')
@ -148,9 +160,10 @@ def draw_prefs_checkout(self, context):
continue continue
if other_repo.url == repo.url: if other_repo.url == repo.url:
sub = col.column() sub = col.column()
sub.alert=True sub.alert = True
sub.label(text="A repository with this URL is already specified.") 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.") sub.label(
text="If you're sure you want to checkout another copy of the repo, feel free to proceed.")
break break
col.prop(repo, 'username', icon='USER') col.prop(repo, 'username', icon='USER')
col.prop(repo, 'password', icon='LOCKED') col.prop(repo, 'password', icon='LOCKED')
@ -159,6 +172,7 @@ def draw_prefs_checkout(self, context):
op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK') op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK')
op_row.operator('svn.checkout_cancel', text="Cancel", icon="X") op_row.operator('svn.checkout_cancel', text="Cancel", icon="X")
def draw_prefs_repos(self, context) -> None: def draw_prefs_repos(self, context) -> None:
layout = self.layout layout = self.layout
@ -224,10 +238,12 @@ def draw_prefs_repos(self, context) -> None:
draw_repo_error(layout, "Directory is not an SVN repository.") draw_repo_error(layout, "Directory is not an SVN repository.")
split = layout.split(factor=0.24) split = layout.split(factor=0.24)
split.row() 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 return
if not self.active_repo.authenticated and not auth_in_progress and not auth_error: 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 return
if len(self.repositories) > 0 and self.active_repo.authenticated: if len(self.repositories) > 0 and self.active_repo.authenticated:
@ -240,16 +256,18 @@ def draw_prefs_repos(self, context) -> None:
layout.label(text="Log: ") layout.label(text="Log: ")
draw_svn_log(context, layout, file_browser=False) draw_svn_log(context, layout, file_browser=False)
def draw_repo_error(layout, message): def draw_repo_error(layout, message):
split = layout.split(factor=0.24) split = layout.split(factor=0.24)
split.row() split.row()
col = split.column() col = split.column()
col.alert=True col.alert = True
col.label(text=message, icon='ERROR') col.label(text=message, icon='ERROR')
registry = [ registry = [
SVN_UL_repositories, SVN_UL_repositories,
SVN_OT_repo_add, SVN_OT_repo_add,
SVN_OT_repo_remove, SVN_OT_repo_remove,
SVN_MT_add_repo SVN_MT_add_repo
] ]

View File

@ -67,4 +67,3 @@ registry = [
VIEW3D_PT_svn_credentials, VIEW3D_PT_svn_credentials,
VIEW3D_PT_svn_files, VIEW3D_PT_svn_files,
] ]

View File

@ -8,12 +8,15 @@ import bpy
package_name = __package__ package_name = __package__
def get_addon_prefs(context): def get_addon_prefs(context):
return context.preferences.addons[__package__].preferences return context.preferences.addons[__package__].preferences
def dots(): def dots():
return "." * int((time() % 10) + 3) return "." * int((time() % 10) + 3)
def redraw_viewport(context=None) -> None: def redraw_viewport(context=None) -> None:
"""This causes the sidebar UI to refresh without having to mouse-hover it.""" """This causes the sidebar UI to refresh without having to mouse-hover it."""
context = bpy.context context = bpy.context