SVN: UX improvements #136
@ -20,7 +20,7 @@ class SVN_Operator:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def update_file_list(context):
|
def update_file_list(context):
|
||||||
repo = context.scene.svn.get_repo(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:
|
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,
|
# Since a status update might already be being requested when an SVN operator is run,
|
||||||
@ -51,7 +51,6 @@ class SVN_Operator_Single_File(SVN_Operator):
|
|||||||
ret = self._execute(context)
|
ret = self._execute(context)
|
||||||
|
|
||||||
file = self.get_file(context)
|
file = self.get_file(context)
|
||||||
if file:
|
|
||||||
Processes.start('Status')
|
Processes.start('Status')
|
||||||
redraw_viewport()
|
redraw_viewport()
|
||||||
|
|
||||||
@ -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
|
return current_blend and current_blend.svn_path == self.file_rel_path
|
||||||
|
|
||||||
reload_file: BoolProperty(
|
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",
|
description="Reload the file after the operation is completed. The UI layout will be preserved",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
@ -133,6 +132,8 @@ class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator):
|
|||||||
super().execute(context)
|
super().execute(context)
|
||||||
if self.reload_file:
|
if self.reload_file:
|
||||||
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False)
|
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False)
|
||||||
|
else:
|
||||||
|
context.scene.svn.file_is_outdated = True
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
|||||||
|
|
||||||
missing_file_allowed = True
|
missing_file_allowed = True
|
||||||
|
|
||||||
revision: IntProperty()
|
revision: IntProperty(default=0)
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
file_entry = context.scene.svn.get_repo(
|
file_entry = context.scene.svn.get_repo(
|
||||||
@ -199,18 +200,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!")
|
"Cancelled: You have local modifications to this file. You must revert or commit it first!")
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
self.execute_svn_command(
|
self.svn_download_file_revision(context, self.file_rel_path, self.revision)
|
||||||
context,
|
|
||||||
["svn", "up", f"-r{self.revision}",
|
|
||||||
f"{self.file_rel_path}", "--accept", "postpone"],
|
|
||||||
use_cred=True
|
|
||||||
)
|
|
||||||
|
|
||||||
self.report({'INFO'},
|
self.report({'INFO'},
|
||||||
f"Checked out revision {self.revision} of {self.file_rel_path}")
|
f"Checked out revision {self.revision} of {self.file_rel_path}")
|
||||||
|
|
||||||
return {"FINISHED"}
|
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"):
|
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
|
||||||
file_entry['revision'] = self.revision
|
file_entry['revision'] = self.revision
|
||||||
latest_rev = repo.get_latest_revision_of_file(self.file_rel_path)
|
latest_rev = repo.get_latest_revision_of_file(self.file_rel_path)
|
||||||
@ -230,13 +237,14 @@ class SVN_OT_restore_file(May_Modifiy_Current_Blend, Operator):
|
|||||||
|
|
||||||
missing_file_allowed = True
|
missing_file_allowed = True
|
||||||
|
|
||||||
def _execute(self, context: Context) -> Set[str]:
|
def svn_revert(self, context, svn_file_path):
|
||||||
self.execute_svn_command(
|
self.execute_svn_command(
|
||||||
context,
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
|
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
|
||||||
@ -251,15 +259,35 @@ class SVN_OT_revert_file(SVN_OT_restore_file):
|
|||||||
|
|
||||||
missing_file_allowed = False
|
missing_file_allowed = False
|
||||||
|
|
||||||
def _execute(self, context: Context) -> Set[str]:
|
|
||||||
super()._execute(context)
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
def get_warning_text(self, context) -> str:
|
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
|
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 = "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
|
||||||
|
|
||||||
|
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):
|
class SVN_OT_add_file(SVN_Operator_Single_File, Operator):
|
||||||
bl_idname = "svn.add_file"
|
bl_idname = "svn.add_file"
|
||||||
bl_label = "Add File"
|
bl_label = "Add File"
|
||||||
@ -305,6 +333,7 @@ class SVN_OT_trash_file(SVN_Operator_Single_File, Warning_Operator, Operator):
|
|||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
file_rel_path: StringProperty()
|
file_rel_path: StringProperty()
|
||||||
|
missing_file_allowed = False
|
||||||
|
|
||||||
def get_warning_text(self, context):
|
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
|
return "Are you sure you want to move this file to the recycle bin?\n " + self.file_rel_path
|
||||||
@ -369,6 +398,7 @@ class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator):
|
|||||||
col.alert = True
|
col.alert = True
|
||||||
col.label(text="Choose which version of the file to keep.")
|
col.label(text="Choose which version of the file to keep.")
|
||||||
col.row().prop(self, 'resolve_method', expand=True)
|
col.row().prop(self, 'resolve_method', expand=True)
|
||||||
|
col.separator()
|
||||||
if self.resolve_method == 'mine-full':
|
if self.resolve_method == 'mine-full':
|
||||||
col.label(text="Local changes will be kept.")
|
col.label(text="Local changes will be kept.")
|
||||||
col.label(
|
col.label(
|
||||||
@ -411,15 +441,14 @@ class SVN_OT_cleanup(SVN_Operator, Operator):
|
|||||||
self.execute_svn_command(context, ["svn", "cleanup"])
|
self.execute_svn_command(context, ["svn", "cleanup"])
|
||||||
repo.reload_svn_log(context)
|
repo.reload_svn_log(context)
|
||||||
|
|
||||||
Processes.kill('Status')
|
|
||||||
Processes.kill('Log')
|
|
||||||
Processes.kill('Commit')
|
Processes.kill('Commit')
|
||||||
Processes.kill('Update')
|
Processes.kill('Update')
|
||||||
Processes.kill('Authenticate')
|
Processes.kill('Authenticate')
|
||||||
Processes.kill('Activate File')
|
Processes.kill('Activate File')
|
||||||
|
|
||||||
Processes.start('Status')
|
Processes.restart('Status')
|
||||||
Processes.start('Log')
|
Processes.restart('Log')
|
||||||
|
Processes.restart('Redraw Viewport')
|
||||||
|
|
||||||
self.report({'INFO'}, "SVN Cleanup complete.")
|
self.report({'INFO'}, "SVN Cleanup complete.")
|
||||||
|
|
||||||
@ -428,13 +457,14 @@ class SVN_OT_cleanup(SVN_Operator, Operator):
|
|||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
SVN_OT_update_single,
|
SVN_OT_update_single,
|
||||||
SVN_OT_download_file_revision,
|
SVN_OT_revert_and_update,
|
||||||
SVN_OT_revert_file,
|
|
||||||
SVN_OT_restore_file,
|
SVN_OT_restore_file,
|
||||||
SVN_OT_unadd_file,
|
SVN_OT_revert_file,
|
||||||
|
SVN_OT_download_file_revision,
|
||||||
SVN_OT_add_file,
|
SVN_OT_add_file,
|
||||||
|
SVN_OT_unadd_file,
|
||||||
SVN_OT_trash_file,
|
SVN_OT_trash_file,
|
||||||
SVN_OT_remove_file,
|
SVN_OT_remove_file,
|
||||||
SVN_OT_cleanup,
|
|
||||||
SVN_OT_resolve_conflict,
|
SVN_OT_resolve_conflict,
|
||||||
|
SVN_OT_cleanup,
|
||||||
]
|
]
|
||||||
|
@ -28,7 +28,6 @@ class SVN_OT_checkout_initiate(Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
prefs = get_addon_prefs(context)
|
prefs = get_addon_prefs(context)
|
||||||
prefs.active_repo_mode = 'SELECTED_REPO'
|
|
||||||
if self.create:
|
if self.create:
|
||||||
prefs.repositories.add()
|
prefs.repositories.add()
|
||||||
prefs.active_repo_idx = len(prefs.repositories)-1
|
prefs.active_repo_idx = len(prefs.repositories)-1
|
||||||
|
@ -40,6 +40,8 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
|
|||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_property = "first_line" # Focus the text input box
|
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
|
# 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
|
# for us to be able to focus the input box automatically when the window pops up
|
||||||
# (see bl_property above)
|
# (see bl_property above)
|
||||||
@ -92,6 +94,7 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
|
|||||||
for f in repo.external_files:
|
for f in repo.external_files:
|
||||||
f.include_in_commit = False
|
f.include_in_commit = False
|
||||||
for f in self.get_committable_files(context):
|
for f in self.get_committable_files(context):
|
||||||
|
if not f.will_conflict:
|
||||||
f.include_in_commit = True
|
f.include_in_commit = True
|
||||||
|
|
||||||
return super().invoke(context, event)
|
return super().invoke(context, event)
|
||||||
@ -108,20 +111,30 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
|
|||||||
row.label(text="Status")
|
row.label(text="Status")
|
||||||
for file in files:
|
for file in files:
|
||||||
row = layout.row()
|
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
|
text = file.status_name
|
||||||
icon = file.status_icon
|
icon = file.status_icon
|
||||||
if file == repo.current_blend_file and self.is_file_really_dirty:
|
if file.will_conflict:
|
||||||
split = row.split(factor=0.7)
|
# We don't want to conflict-resolve during a commit, it's
|
||||||
row = split.row()
|
# confusing. User should resolve this as a separate step.
|
||||||
row.alert = True
|
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!"
|
text += " but not saved!"
|
||||||
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',
|
op_row.operator('svn.save_during_commit',
|
||||||
icon='FILE_BLEND', text="Save")
|
icon='FILE_BLEND', text="Save")
|
||||||
row.label(text=text, icon=icon)
|
status_ui.label(text=text, icon=icon)
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.label(text="Commit message:")
|
row.label(text="Commit message:")
|
||||||
@ -142,7 +155,6 @@ class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
|
|||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
committable_files = self.get_committable_files(context)
|
committable_files = self.get_committable_files(context)
|
||||||
files_to_commit = [f for f in committable_files if f.include_in_commit]
|
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)
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
|
||||||
if not files_to_commit:
|
if not files_to_commit:
|
||||||
|
@ -44,11 +44,14 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
|||||||
current_blend = repo.current_blend_file
|
current_blend = repo.current_blend_file
|
||||||
if self.revision == 0:
|
if self.revision == 0:
|
||||||
if current_blend and current_blend.repos_status != 'none':
|
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
|
self.file_rel_path = current_blend.svn_path
|
||||||
return context.window_manager.invoke_props_dialog(self, width=500)
|
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||||
else:
|
else:
|
||||||
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']:
|
||||||
|
# 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 context.window_manager.invoke_props_dialog(self, width=500)
|
||||||
|
|
||||||
return self.execute(context)
|
return self.execute(context)
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
# (c) 2022, Blender Foundation - Demeter Dzadik
|
# (c) 2022, Blender Foundation - Demeter Dzadik
|
||||||
|
|
||||||
from typing import Optional, Any, Set, Tuple, List
|
from typing import Optional, Any, Set, Tuple, List
|
||||||
|
import platform
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import IntProperty, CollectionProperty, BoolProperty, EnumProperty
|
from bpy.props import IntProperty, CollectionProperty, BoolProperty, EnumProperty
|
||||||
from bpy.types import AddonPreferences
|
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 .repository import SVN_repository
|
||||||
from .svn_info import get_svn_info
|
from .svn_info import get_svn_info
|
||||||
import json
|
import json
|
||||||
@ -19,8 +20,25 @@ from .threaded.background_process import Processes
|
|||||||
class SVN_addon_preferences(AddonPreferences):
|
class SVN_addon_preferences(AddonPreferences):
|
||||||
bl_idname = __package__
|
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)
|
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):
|
def init_repo(self, context, repo_path: Path or str):
|
||||||
"""Attempt to initialize a repository based on a directory.
|
"""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.
|
This means executing `svn info` in the repo_path to get the URL and root dir.
|
||||||
@ -40,70 +58,45 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
|
|
||||||
repo = self.repositories.add()
|
repo = self.repositories.add()
|
||||||
repo.initialize(root_dir, base_url)
|
repo.initialize(root_dir, base_url)
|
||||||
|
self.active_repo_idx = len(self.repositories)-1
|
||||||
|
|
||||||
return repo
|
return repo
|
||||||
|
|
||||||
def update_active_repo_idx(self, context):
|
|
||||||
if self.idx_updating or 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
|
|
||||||
|
|
||||||
if (
|
|
||||||
active_repo and
|
|
||||||
not active_repo.authenticated and
|
|
||||||
not active_repo.auth_failed and
|
|
||||||
active_repo.is_cred_entered
|
|
||||||
):
|
|
||||||
active_repo.authenticate(context)
|
|
||||||
|
|
||||||
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(
|
checkout_mode: BoolProperty(
|
||||||
name="Checkout In Progress",
|
name="Checkout In Progress",
|
||||||
description="Internal flag to indicate that the user is currently trying to create a new checkout",
|
description="Internal flag to indicate that the user is currently trying to create a new checkout",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
active_repo_mode: EnumProperty(
|
def update_active_repo_idx(self, context):
|
||||||
name="Choose Repository",
|
if len(self.repositories) == 0:
|
||||||
description="Whether the add-on should communicate with the repository of the currently opened .blend file, or the repository selected in the list below",
|
return
|
||||||
items=[
|
active_repo = self.active_repo
|
||||||
('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",
|
# Authenticate when switching repos.
|
||||||
"Communicate with the selected repository")
|
if (
|
||||||
],
|
active_repo and
|
||||||
default='CURRENT_BLEND',
|
not active_repo.auth_failed and
|
||||||
update=update_active_repo_mode
|
active_repo.is_cred_entered
|
||||||
)
|
):
|
||||||
|
Processes.start('Redraw Viewport')
|
||||||
|
if active_repo.authenticated:
|
||||||
|
Processes.restart('Status')
|
||||||
|
else:
|
||||||
|
active_repo.authenticate()
|
||||||
|
else:
|
||||||
|
Processes.kill('Status')
|
||||||
|
|
||||||
active_repo_idx: IntProperty(
|
active_repo_idx: IntProperty(
|
||||||
name="SVN Repositories",
|
name="SVN Repositories",
|
||||||
options=set(),
|
options=set(),
|
||||||
update=update_active_repo_idx
|
update=update_active_repo_idx
|
||||||
)
|
)
|
||||||
idx_updating: BoolProperty(
|
|
||||||
name="Index is Updating",
|
|
||||||
description="Helper flag to avoid infinite looping update callbacks",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@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:
|
if 0 <= self.active_repo_idx <= len(self.repositories)-1:
|
||||||
return self.repositories[self.active_repo_idx]
|
return self.repositories[self.active_repo_idx]
|
||||||
|
|
||||||
@ -163,7 +156,33 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
self.load_repo_info_from_file()
|
self.load_repo_info_from_file()
|
||||||
self.save_repo_info_to_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 = [
|
registry = [
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
# (c) 2022, Blender Foundation - Demeter Dzadik
|
# (c) 2022, Blender Foundation - Demeter Dzadik
|
||||||
|
|
||||||
from .util import get_addon_prefs
|
from .util import get_addon_prefs
|
||||||
from bpy.props import StringProperty, PointerProperty
|
from bpy.props import StringProperty, PointerProperty, BoolProperty
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional, Dict, Any, List, Tuple, Set
|
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.
|
||||||
@ -27,27 +26,17 @@ class SVN_scene_properties(PropertyGroup):
|
|||||||
description="Absolute directory path of the SVN repository's root in the file system",
|
description="Absolute directory path of the SVN repository's root in the file system",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_repo(self, context):
|
file_is_outdated: BoolProperty(
|
||||||
"""Return the current repository.
|
name="File Is Outdated",
|
||||||
Depending on preferences, this is either the repo the current .blend file is in,
|
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",
|
||||||
or whatever repo is selected in the preferences UI.
|
default=False
|
||||||
"""
|
)
|
||||||
prefs = get_addon_prefs(context)
|
|
||||||
|
|
||||||
if prefs.active_repo_mode == 'CURRENT_BLEND':
|
def get_repo(self, context) -> Optional['SVN_repository']:
|
||||||
return self.get_scene_repo(context)
|
"""Return the active repository."""
|
||||||
else:
|
prefs = get_addon_prefs(context)
|
||||||
return prefs.active_repo
|
return prefs.active_repo
|
||||||
|
|
||||||
def get_scene_repo(self, context) -> Optional['SVN_repository']:
|
|
||||||
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 = [
|
registry = [
|
||||||
SVN_scene_properties,
|
SVN_scene_properties,
|
||||||
|
@ -48,6 +48,10 @@ class SVN_file(PropertyGroup):
|
|||||||
default="none",
|
default="none",
|
||||||
options=set()
|
options=set()
|
||||||
)
|
)
|
||||||
|
@property
|
||||||
|
def will_conflict(self):
|
||||||
|
return self.status != 'normal' and self.repos_status != 'none'
|
||||||
|
|
||||||
status_prediction_type: EnumProperty(
|
status_prediction_type: EnumProperty(
|
||||||
name="Status Predicted By Process",
|
name="Status Predicted By Process",
|
||||||
items=[
|
items=[
|
||||||
@ -68,18 +72,6 @@ class SVN_file(PropertyGroup):
|
|||||||
options=set()
|
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
|
@property
|
||||||
def is_outdated(self):
|
def is_outdated(self):
|
||||||
return self.repos_status == 'modified' and self.status == 'normal'
|
return self.repos_status == 'modified' and self.status == 'normal'
|
||||||
@ -247,6 +239,7 @@ class SVN_repository(PropertyGroup):
|
|||||||
@property
|
@property
|
||||||
def is_valid_svn(self):
|
def is_valid_svn(self):
|
||||||
dir_path = Path(self.directory)
|
dir_path = Path(self.directory)
|
||||||
|
# TODO: This property is checked pretty often, so we run `svn info` pretty often. Might not be a big deal, but maybe it's a bit overkill?
|
||||||
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
|
||||||
@ -282,10 +275,10 @@ class SVN_repository(PropertyGroup):
|
|||||||
if get_addon_prefs(context).loading:
|
if get_addon_prefs(context).loading:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.authenticate(context)
|
self.authenticate()
|
||||||
self.update_repo_info_file(context)
|
self.update_repo_info_file(context)
|
||||||
|
|
||||||
def authenticate(self, context):
|
def authenticate(self):
|
||||||
self.auth_failed = False
|
self.auth_failed = False
|
||||||
if self.is_valid_svn and self.is_cred_entered:
|
if self.is_valid_svn and self.is_cred_entered:
|
||||||
Processes.start('Authenticate')
|
Processes.start('Authenticate')
|
||||||
@ -305,9 +298,9 @@ class SVN_repository(PropertyGroup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cred_entered(self):
|
def is_cred_entered(self) -> bool:
|
||||||
"""Check if there's a username and password entered at all."""
|
"""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(
|
authenticated: BoolProperty(
|
||||||
name="Authenticated",
|
name="Authenticated",
|
||||||
@ -316,7 +309,7 @@ class SVN_repository(PropertyGroup):
|
|||||||
)
|
)
|
||||||
auth_failed: BoolProperty(
|
auth_failed: BoolProperty(
|
||||||
name="Authentication Failed",
|
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
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -342,10 +335,6 @@ class SVN_repository(PropertyGroup):
|
|||||||
name="SVN Log",
|
name="SVN Log",
|
||||||
options=set()
|
options=set()
|
||||||
)
|
)
|
||||||
log_active_index_filebrowser: IntProperty(
|
|
||||||
name="SVN Log",
|
|
||||||
options=set()
|
|
||||||
)
|
|
||||||
|
|
||||||
reload_svn_log = svn_log.reload_svn_log
|
reload_svn_log = svn_log.reload_svn_log
|
||||||
|
|
||||||
@ -360,19 +349,13 @@ class SVN_repository(PropertyGroup):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
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]:
|
def get_log_by_revision(self, revision: int) -> Tuple[int, SVN_log]:
|
||||||
for i, log in enumerate(self.log):
|
for i, log in enumerate(self.log):
|
||||||
if log.revision_number == revision:
|
if log.revision_number == revision:
|
||||||
return i, log
|
return i, log
|
||||||
|
|
||||||
def get_latest_revision_of_file(self, svn_path: str) -> int:
|
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)
|
svn_path = str(svn_path)
|
||||||
for log in reversed(self.log):
|
for log in reversed(self.log):
|
||||||
for changed_file in log.changed_files:
|
for changed_file in log.changed_files:
|
||||||
@ -442,33 +425,43 @@ class SVN_repository(PropertyGroup):
|
|||||||
|
|
||||||
def update_active_file(self, context):
|
def update_active_file(self, context):
|
||||||
"""When user clicks on a different file, the latest log entry of that file
|
"""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!
|
||||||
|
"""
|
||||||
|
|
||||||
latest_idx = self.get_latest_revision_of_file(
|
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)
|
self.active_file.svn_path)
|
||||||
# SVN Revisions are not 0-indexed, so we need to subtract 1.
|
# 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
|
space = context.space_data
|
||||||
if space and space.type == 'FILE_BROWSER':
|
if space and space.type == 'FILE_BROWSER':
|
||||||
# Set the active file in the file browser to whatever was selected in the SVN Files panel.
|
space.params.directory = Path(self.active_file.absolute_path).parent.as_posix().encode()
|
||||||
self.log_active_index_filebrowser = latest_idx-1
|
|
||||||
|
|
||||||
space.params.directory = self.active_file.absolute_path.parent.as_posix().encode()
|
|
||||||
space.params.filename = self.active_file.name.encode()
|
space.params.filename = self.active_file.name.encode()
|
||||||
|
|
||||||
space.deselect_all()
|
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)
|
relative_path=self.active_file.name)
|
||||||
Processes.start('Activate File')
|
Processes.start('Activate File') # This is my work-around.
|
||||||
|
|
||||||
# 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(
|
self.log.foreach_set(
|
||||||
'affects_active_file',
|
'affects_active_file',
|
||||||
[log_entry.changes_file(self.active_file)
|
[log_entry.changes_file(self.active_file)
|
||||||
for log_entry in self.log]
|
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(
|
external_files_active_index: IntProperty(
|
||||||
name="File List",
|
name="File List",
|
||||||
description="Files tracked by SVN",
|
description="Files tracked by SVN",
|
||||||
@ -513,28 +506,10 @@ 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,
|
def refresh_ui_lists(self, context):
|
||||||
# but then they cannot be accessed from anywhere else,
|
"""Refresh the file UI list based on filter settings.
|
||||||
# since template_list() does not return the UIList instance.
|
Also triggers a refresh of the SVN UIList, through the update callback of
|
||||||
# We need to be able to access them outside of drawing code, to be able to
|
external_files_active_index."""
|
||||||
# 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."""
|
|
||||||
|
|
||||||
UI_LIST = bpy.types.UI_UL_list
|
UI_LIST = bpy.types.UI_UL_list
|
||||||
if self.file_search_filter:
|
if self.file_search_filter:
|
||||||
@ -555,12 +530,20 @@ class SVN_repository(PropertyGroup):
|
|||||||
|
|
||||||
file.show_in_filelist = not file.has_default_status
|
file.show_in_filelist = not file.has_default_status
|
||||||
|
|
||||||
self.force_good_active_index(context)
|
if len(self.external_files) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make sure the active file isn't now being filtered out.
|
||||||
|
# If it is, change the active file to the first visible one.
|
||||||
|
for i, file in enumerate(self.external_files):
|
||||||
|
if file.show_in_filelist:
|
||||||
|
self.external_files_active_index = i
|
||||||
|
return
|
||||||
|
|
||||||
file_search_filter: StringProperty(
|
file_search_filter: StringProperty(
|
||||||
name="Search Filter",
|
name="Search Filter",
|
||||||
description="Only show entries that contain this string",
|
description="Only show entries that contain this string",
|
||||||
update=update_file_filter
|
update=refresh_ui_lists
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class BackgroundProcess:
|
|||||||
# Displayed in the tooltip on mouse-hover in the error message when an error occurs.
|
# Displayed in the tooltip on mouse-hover in the error message when an error occurs.
|
||||||
error_description = "SVN Error:"
|
error_description = "SVN Error:"
|
||||||
|
|
||||||
debug = True
|
debug = False
|
||||||
|
|
||||||
def debug_print(self, msg: str):
|
def debug_print(self, msg: str):
|
||||||
if self.debug:
|
if self.debug:
|
||||||
@ -82,7 +82,7 @@ class BackgroundProcess:
|
|||||||
def handle_error(self, context, error):
|
def handle_error(self, context, error):
|
||||||
self.output = ""
|
self.output = ""
|
||||||
self.error = error.stderr.decode()
|
self.error = error.stderr.decode()
|
||||||
self.is_running = False
|
self.stop()
|
||||||
|
|
||||||
def process_output(self, context, prefs):
|
def process_output(self, context, prefs):
|
||||||
"""
|
"""
|
||||||
@ -113,7 +113,7 @@ class BackgroundProcess:
|
|||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
if not repo:
|
if not repo:
|
||||||
self.debug_print("Shutdown: Not in repo.")
|
self.debug_print("Shutdown: Not in repo.")
|
||||||
self.is_running = False
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
prefs = get_addon_prefs(context)
|
prefs = get_addon_prefs(context)
|
||||||
@ -127,7 +127,7 @@ class BackgroundProcess:
|
|||||||
|
|
||||||
if self.needs_authentication and not repo.authenticated:
|
if self.needs_authentication and not repo.authenticated:
|
||||||
self.debug_print("Shutdown: Authentication needed.")
|
self.debug_print("Shutdown: Authentication needed.")
|
||||||
self.is_running = False
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.thread or not self.thread.is_alive() and not self.output and not self.error:
|
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 = ""
|
self.output = ""
|
||||||
redraw_viewport()
|
redraw_viewport()
|
||||||
if self.repeat_delay == 0:
|
if self.repeat_delay == 0:
|
||||||
self.debug_print(
|
self.debug_print("Shutdown: Output was processed, repeat_delay==0.")
|
||||||
"Shutdown: Output was processed, repeat_delay==0.")
|
self.stop()
|
||||||
self.is_running = False
|
|
||||||
return
|
return
|
||||||
self.debug_print(f"Processed output. Waiting {self.repeat_delay}")
|
self.debug_print(f"Processed output. Waiting {self.repeat_delay}")
|
||||||
return self.repeat_delay
|
return self.repeat_delay
|
||||||
elif not self.thread and not self.thread.is_alive() and self.repeat_delay == 0:
|
elif not self.thread and not self.thread.is_alive() and self.repeat_delay == 0:
|
||||||
self.debug_print("Shutdown: Finished.\n")
|
self.debug_print("Shutdown: Finished.\n")
|
||||||
self.is_running = False
|
self.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.debug_print(f"Tick delay: {self.tick_delay}")
|
self.debug_print(f"Tick delay: {self.tick_delay}")
|
||||||
@ -188,6 +187,7 @@ class BackgroundProcess:
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the process if it isn't running, by unregistering its timer function"""
|
"""Stop the process if it isn't running, by unregistering its timer function"""
|
||||||
|
self.debug_print("stop() function was called.")
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
if bpy.app.timers.is_registered(self.timer_function):
|
if bpy.app.timers.is_registered(self.timer_function):
|
||||||
# This won't work if the timer has returned None at any point, as that
|
# This won't work if the timer has returned None at any point, as that
|
||||||
@ -214,6 +214,7 @@ class ProcessManager:
|
|||||||
def processes(self):
|
def processes(self):
|
||||||
# I tried to implement this thing as a Singleton that inherits from the `dict` class,
|
# 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 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,
|
# 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,
|
# 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.
|
# but when I try to access it from anywhere, it's just empty. My mind is boggled.
|
||||||
@ -238,6 +239,8 @@ class ProcessManager:
|
|||||||
process = self.processes.get(proc_name, None)
|
process = self.processes.get(proc_name, None)
|
||||||
if process:
|
if process:
|
||||||
process.start()
|
process.start()
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(process, key, value)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
for subcl in get_recursive_subclasses(BackgroundProcess):
|
for subcl in get_recursive_subclasses(BackgroundProcess):
|
||||||
@ -261,6 +264,12 @@ class ProcessManager:
|
|||||||
process.stop()
|
process.stop()
|
||||||
del self.processes[proc_name]
|
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.
|
# I named this variable with title-case, to indicate that it's a Singleton.
|
||||||
# There should only be one.
|
# There should only be one.
|
||||||
|
@ -51,6 +51,8 @@ class BGP_SVN_Commit(BackgroundProcess):
|
|||||||
print(self.output)
|
print(self.output)
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
for f in repo.external_files:
|
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':
|
if f.status_prediction_type == 'SVN_COMMIT':
|
||||||
f.status_prediction_type = 'SKIP_ONCE'
|
f.status_prediction_type = 'SKIP_ONCE'
|
||||||
Processes.start('Log')
|
Processes.start('Log')
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
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!"
|
||||||
@ -29,6 +28,9 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri
|
|||||||
SVN root.
|
SVN root.
|
||||||
"""
|
"""
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
if "svn" not in command:
|
||||||
|
command.insert(0, "svn")
|
||||||
|
|
||||||
if use_cred:
|
if use_cred:
|
||||||
command += get_credential_commands(context)
|
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(f"Command returned error: {command}")
|
||||||
print(err_msg)
|
print(err_msg)
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
|
def check_svn_installed():
|
||||||
|
code, message = subprocess.getstatusoutput('svn')
|
||||||
|
return code != 127
|
@ -7,6 +7,7 @@ class BGP_SVN_Redraw_Viewport(BackgroundProcess):
|
|||||||
repeat_delay = 1
|
repeat_delay = 1
|
||||||
debug = False
|
debug = False
|
||||||
tick_delay = 1
|
tick_delay = 1
|
||||||
|
needs_authentication = False
|
||||||
|
|
||||||
def tick(self, context, prefs):
|
def tick(self, context, prefs):
|
||||||
redraw_viewport()
|
redraw_viewport()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from ..svn_info import get_svn_info
|
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
|
||||||
from .execute_subprocess import execute_svn_command
|
from .execute_subprocess import execute_svn_command, check_svn_installed
|
||||||
from .background_process import BackgroundProcess, Processes
|
from .background_process import BackgroundProcess, Processes
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty
|
||||||
@ -55,49 +55,44 @@ class SVN_OT_explain_status(Operator):
|
|||||||
|
|
||||||
|
|
||||||
@bpy.app.handlers.persistent
|
@bpy.app.handlers.persistent
|
||||||
def init_svn_of_current_file(_scene=None):
|
def ensure_svn_of_current_file(_scene=None):
|
||||||
"""When opening or saving a .blend file:
|
"""When opening or saving a .blend file, it's possible that the new .blend
|
||||||
- Initialize SVN Scene info
|
is part of an SVN repository. If this is the case, do the following:
|
||||||
- Initialize Repository
|
- Check if this file's repository is already in our database
|
||||||
- Try to authenticate
|
- If not, create it
|
||||||
|
- Switch to that repo
|
||||||
"""
|
"""
|
||||||
|
|
||||||
context = bpy.context
|
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
|
scene_svn = context.scene.svn
|
||||||
|
|
||||||
prefs = get_addon_prefs(context)
|
old_active_repo = prefs.active_repo
|
||||||
prefs.sync_repo_info_file()
|
prefs.init_repo_list()
|
||||||
|
|
||||||
for repo in prefs.repositories:
|
# If the file is unsaved, nothing more to do.
|
||||||
# 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:
|
if not bpy.data.filepath:
|
||||||
scene_svn.svn_url = ""
|
scene_svn.svn_url = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If file is not in a repo, nothing more to do.
|
||||||
is_in_repo = set_scene_svn_info(context)
|
is_in_repo = set_scene_svn_info(context)
|
||||||
if not is_in_repo:
|
if not is_in_repo:
|
||||||
return
|
return
|
||||||
|
|
||||||
repo = scene_svn.get_scene_repo(context)
|
# If file is in an existing repo, we should switch over to that repo.
|
||||||
if not repo:
|
for i, existing_repo in enumerate(prefs.repositories):
|
||||||
repo = prefs.init_repo(context, scene_svn.svn_directory)
|
if ( existing_repo.url == scene_svn.svn_url and
|
||||||
|
existing_repo.directory == scene_svn.svn_directory and
|
||||||
for i, other_repo in enumerate(prefs.repositories):
|
existing_repo != old_active_repo
|
||||||
if other_repo == repo:
|
):
|
||||||
prefs.active_repo_idx = i
|
prefs.active_repo_idx = i
|
||||||
|
|
||||||
else:
|
else:
|
||||||
repo = prefs.active_repo
|
# If file is in a non-existing repo, initialize that repo.
|
||||||
if not repo:
|
prefs.init_repo(context, scene_svn.svn_directory)
|
||||||
return
|
|
||||||
|
|
||||||
if repo.is_cred_entered:
|
|
||||||
repo.authenticate(context)
|
|
||||||
|
|
||||||
|
|
||||||
def set_scene_svn_info(context) -> bool:
|
def set_scene_svn_info(context) -> bool:
|
||||||
@ -233,21 +228,6 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]):
|
|||||||
if not file_entry.exists:
|
if not file_entry.exists:
|
||||||
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
||||||
|
|
||||||
# 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'):
|
if entry_existed and (file_entry.repos_status == 'none' and repos_status != 'none'):
|
||||||
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
||||||
|
|
||||||
@ -277,8 +257,7 @@ def update_file_list(context, file_statuses: Dict[str, Tuple[str, str, int]]):
|
|||||||
if file_entry.svn_path not in svn_paths:
|
if file_entry.svn_path not in svn_paths:
|
||||||
repo.remove_file_entry(file_entry)
|
repo.remove_file_entry(file_entry)
|
||||||
|
|
||||||
repo.update_file_filter(context)
|
repo.refresh_ui_lists(context)
|
||||||
repo.force_good_active_index(context)
|
|
||||||
|
|
||||||
|
|
||||||
def get_repo_file_statuses(svn_status_str: str) -> Dict[str, Tuple[str, str, int]]:
|
def get_repo_file_statuses(svn_status_str: str) -> Dict[str, Tuple[str, str, int]]:
|
||||||
@ -339,22 +318,22 @@ def mark_current_file_as_modified(_dummy1=None, _dummy2=None):
|
|||||||
|
|
||||||
|
|
||||||
def delayed_init_svn(delay=1):
|
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():
|
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)
|
bpy.app.handlers.save_post.append(mark_current_file_as_modified)
|
||||||
|
|
||||||
delayed_init_svn()
|
delayed_init_svn()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
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)
|
bpy.app.handlers.save_post.remove(mark_current_file_as_modified)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from . import (
|
|||||||
ui_sidebar,
|
ui_sidebar,
|
||||||
ui_filebrowser,
|
ui_filebrowser,
|
||||||
ui_log,
|
ui_log,
|
||||||
ui_prefs,
|
ui_repo_list,
|
||||||
ui_outdated_warning,
|
ui_outdated_warning,
|
||||||
ui_context_menus
|
ui_context_menus
|
||||||
)
|
)
|
||||||
@ -13,7 +13,7 @@ modules = [
|
|||||||
ui_sidebar,
|
ui_sidebar,
|
||||||
ui_filebrowser,
|
ui_filebrowser,
|
||||||
ui_log,
|
ui_log,
|
||||||
ui_prefs,
|
ui_repo_list,
|
||||||
ui_outdated_warning,
|
ui_outdated_warning,
|
||||||
ui_context_menus
|
ui_context_menus
|
||||||
]
|
]
|
||||||
|
@ -3,26 +3,10 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Context, UIList, Operator
|
from bpy.types import Context, UIList, Operator
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty, BoolProperty
|
||||||
from pathlib import Path
|
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:
|
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
|
"""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.
|
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()
|
layout.separator()
|
||||||
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",
|
op = layout.operator("wm.open_mainfile",
|
||||||
text=f"Open {active_file.name}").filepath = active_file.absolute_path
|
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:
|
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))
|
||||||
@ -53,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'):
|
if not check_context_match(context, 'ui_list', 'SVN_UL_log'):
|
||||||
return
|
return
|
||||||
|
|
||||||
is_filebrowser = context.space_data.type == 'FILE_BROWSER'
|
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
repo = context.scene.svn.get_repo(context)
|
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",
|
layout.operator("svn.update_all",
|
||||||
text=f"Revert Repository To r{active_log.revision_number}").revision = active_log.revision_number
|
text=f"Revert Repository To r{active_log.revision_number}").revision = active_log.revision_number
|
||||||
layout.separator()
|
layout.separator()
|
||||||
@ -73,5 +65,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_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]
|
|
||||||
|
@ -114,6 +114,7 @@ class SVN_UL_file_list(UIList):
|
|||||||
explainer.status = status
|
explainer.status = status
|
||||||
explainer.file_rel_path = file_entry.svn_path
|
explainer.file_rel_path = file_entry.svn_path
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cls_filter_items(cls, context, data, propname):
|
def cls_filter_items(cls, context, data, propname):
|
||||||
"""By moving all of this logic to a classmethod (and all the filter
|
"""By moving all of this logic to a classmethod (and all the filter
|
||||||
@ -148,8 +149,10 @@ 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="")
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
if repo:
|
||||||
|
row.prop(repo, 'file_search_filter', text="")
|
||||||
|
|
||||||
|
|
||||||
def draw_process_info(context, layout):
|
def draw_process_info(context, layout):
|
||||||
@ -183,18 +186,19 @@ def draw_process_info(context, layout):
|
|||||||
", ".join([p.name for p in Processes.running_processes]))
|
", ".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:
|
if not repo:
|
||||||
return
|
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_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()
|
main_row = main_col.row()
|
||||||
split = main_row.split(factor=0.6)
|
split = main_row.split(factor=0.6)
|
||||||
filepath_row = split.row()
|
filepath_row = split.row()
|
||||||
@ -207,11 +211,6 @@ def draw_repo_file_list(context, layout, repo):
|
|||||||
ops_row.alignment = 'RIGHT'
|
ops_row.alignment = 'RIGHT'
|
||||||
ops_row.label(text="Operations")
|
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 = main_col.row()
|
||||||
row.template_list(
|
row.template_list(
|
||||||
"SVN_UL_file_list",
|
"SVN_UL_file_list",
|
||||||
|
@ -4,11 +4,10 @@
|
|||||||
from bpy.types import Panel
|
from bpy.types import Panel
|
||||||
from bl_ui.space_filebrowser import FileBrowserPanel
|
from bl_ui.space_filebrowser import FileBrowserPanel
|
||||||
|
|
||||||
from .ui_log import draw_svn_log
|
from .ui_log import draw_svn_log, is_log_useful
|
||||||
from .ui_file_list import draw_repo_file_list
|
from .ui_file_list import draw_file_list
|
||||||
from ..util import get_addon_prefs
|
from ..util import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel):
|
class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel):
|
||||||
bl_space_type = 'FILE_BROWSER'
|
bl_space_type = 'FILE_BROWSER'
|
||||||
bl_region_type = 'TOOLS'
|
bl_region_type = 'TOOLS'
|
||||||
@ -20,47 +19,37 @@ class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel):
|
|||||||
if not super().poll(context):
|
if not super().poll(context):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
repo = context.scene.svn.get_repo(context)
|
prefs = get_addon_prefs(context)
|
||||||
if not repo:
|
return prefs.active_repo and prefs.active_repo.authenticated
|
||||||
return False
|
|
||||||
|
|
||||||
return repo.is_filebrowser_directory_in_repo(context)
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
# TODO: Get repository of the current file browser's directory.
|
draw_file_list(context, layout)
|
||||||
prefs = get_addon_prefs(context)
|
|
||||||
if len(prefs.repositories) > 0:
|
|
||||||
repo = prefs.active_repo
|
|
||||||
draw_repo_file_list(context, layout, repo)
|
|
||||||
|
|
||||||
|
|
||||||
class FILEBROWSER_PT_SVN_log(FileBrowserPanel, Panel):
|
class FILEBROWSER_PT_SVN_log(FileBrowserPanel, Panel):
|
||||||
bl_space_type = 'FILE_BROWSER'
|
bl_space_type = 'FILE_BROWSER'
|
||||||
bl_region_type = 'TOOLS'
|
bl_region_type = 'TOOLS'
|
||||||
bl_category = "Bookmarks"
|
bl_category = "Bookmarks"
|
||||||
bl_label = "SVN Log"
|
bl_parent_id = "FILEBROWSER_PT_SVN_files"
|
||||||
|
bl_label = "Revision History"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if not super().poll(context):
|
if not super().poll(context):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
repo = context.scene.svn.get_repo(context)
|
return is_log_useful(context)
|
||||||
if not repo:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return repo.get_filebrowser_active_file(context)
|
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
draw_svn_log(context, layout, file_browser=True)
|
draw_svn_log(context, layout)
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from bpy.props import IntProperty, BoolProperty
|
from bpy.props import IntProperty, BoolProperty
|
||||||
from bpy.types import UIList, Panel, Operator
|
from bpy.types import UIList, Panel, Operator
|
||||||
|
from ..util import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
class SVN_UL_log(UIList):
|
class SVN_UL_log(UIList):
|
||||||
@ -21,9 +22,7 @@ class SVN_UL_log(UIList):
|
|||||||
|
|
||||||
num, auth, date, msg = layout_log_split(layout.row())
|
num, auth, date, msg = layout_log_split(layout.row())
|
||||||
|
|
||||||
is_filebrowser = context.space_data.type == 'FILE_BROWSER'
|
active_file = svn.active_file
|
||||||
active_file = svn.get_filebrowser_active_file(
|
|
||||||
context) if is_filebrowser else svn.active_file
|
|
||||||
num.label(text=str(log_entry.revision_number))
|
num.label(text=str(log_entry.revision_number))
|
||||||
if item.revision_number == active_file.revision:
|
if item.revision_number == active_file.revision:
|
||||||
num.operator('svn.tooltip_log', text="", icon='LAYER_ACTIVE',
|
num.operator('svn.tooltip_log', text="", icon='LAYER_ACTIVE',
|
||||||
@ -88,8 +87,15 @@ class SVN_UL_log(UIList):
|
|||||||
toggle=True, icon='ALIGN_JUSTIFY')
|
toggle=True, icon='ALIGN_JUSTIFY')
|
||||||
|
|
||||||
|
|
||||||
def is_log_useful(context):
|
def is_log_useful(context) -> bool:
|
||||||
repo = context.scene.svn.get_repo(context)
|
"""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:
|
if len(repo.log) == 0 or len(repo.external_files) == 0:
|
||||||
return False
|
return False
|
||||||
active_file = repo.active_file
|
active_file = repo.active_file
|
||||||
@ -121,7 +127,7 @@ class VIEW3D_PT_svn_log(Panel):
|
|||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
draw_svn_log(context, layout, file_browser=False)
|
draw_svn_log(context, layout)
|
||||||
|
|
||||||
|
|
||||||
def layout_log_split(layout):
|
def layout_log_split(layout):
|
||||||
@ -140,23 +146,25 @@ def layout_log_split(layout):
|
|||||||
return num, auth, date, msg
|
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, auth, date, msg = layout_log_split(layout.row())
|
||||||
num.label(text="Rev. #")
|
num.label(text="Rev. #")
|
||||||
auth.label(text="Author")
|
auth.label(text="Author")
|
||||||
date.label(text="Date")
|
date.label(text="Date")
|
||||||
msg.label(text="Message")
|
msg.label(text="Message")
|
||||||
repo = context.scene.svn.get_repo(context)
|
|
||||||
|
prefs = get_addon_prefs(context)
|
||||||
|
repo = prefs.active_repo
|
||||||
layout.template_list(
|
layout.template_list(
|
||||||
"SVN_UL_log",
|
"SVN_UL_log",
|
||||||
"svn_log",
|
"svn_log",
|
||||||
repo,
|
repo,
|
||||||
"log",
|
"log",
|
||||||
repo,
|
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:
|
if not active_log:
|
||||||
return
|
return
|
||||||
layout.label(text="Revision Date: " + active_log.revision_date)
|
layout.label(text="Revision Date: " + active_log.revision_date)
|
||||||
@ -187,9 +195,6 @@ def execute_tooltip_log(self, context):
|
|||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
tup = repo.get_log_by_revision(self.log_rev)
|
tup = repo.get_log_by_revision(self.log_rev)
|
||||||
if tup:
|
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'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -17,9 +17,6 @@ def draw_outdated_file_warning(self, context):
|
|||||||
# If the current file is not in an SVN repository.
|
# If the current file is not in an SVN repository.
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_file.status == 'normal' and current_file.repos_status == 'none':
|
|
||||||
return
|
|
||||||
|
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
@ -27,15 +24,14 @@ def draw_outdated_file_warning(self, context):
|
|||||||
if current_file.status == 'conflicted':
|
if current_file.status == 'conflicted':
|
||||||
row.operator('svn.resolve_conflict',
|
row.operator('svn.resolve_conflict',
|
||||||
text="SVN: This .blend file is conflicted.", icon='ERROR')
|
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:
|
||||||
warning = row.operator(
|
op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file may be outdated.", icon='ERROR')
|
||||||
'svn.custom_tooltip', text="SVN: This .blend file is outdated.", icon='ERROR')
|
op.file_rel_path = repo.current_blend_file.svn_path
|
||||||
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.TOPBAR_MT_editor_menus.append(draw_outdated_file_warning)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
bpy.types.VIEW3D_HT_header.remove(draw_outdated_file_warning)
|
bpy.types.TOPBAR_MT_editor_menus.remove(draw_outdated_file_warning)
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# (c) 2023, Blender Foundation - Demeter Dzadik
|
# (c) 2023, Blender Foundation - Demeter Dzadik
|
||||||
|
|
||||||
from pathlib import Path
|
import platform
|
||||||
|
|
||||||
from bpy.types import UIList, Operator, Menu
|
from bpy.types import UIList, Operator, Menu
|
||||||
from bpy_extras.io_utils import ImportHelper
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
from ..util import get_addon_prefs
|
from ..util import get_addon_prefs
|
||||||
from .ui_log import draw_svn_log, is_log_useful
|
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 ..threaded.background_process import Processes
|
||||||
import platform
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
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
|
||||||
row = layout.row()
|
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)
|
row.label(text=repo.display_name)
|
||||||
|
|
||||||
if not repo.dir_exists:
|
if not repo.dir_exists:
|
||||||
@ -30,6 +29,7 @@ class SVN_UL_repositories(UIList):
|
|||||||
|
|
||||||
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'}
|
||||||
|
|
||||||
bl_idname = "svn.repo_add"
|
bl_idname = "svn.repo_add"
|
||||||
@ -40,7 +40,16 @@ class SVN_OT_repo_add(Operator, ImportHelper):
|
|||||||
repos = prefs.repositories
|
repos = prefs.repositories
|
||||||
|
|
||||||
path = Path(self.filepath)
|
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():
|
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
|
path = path.parent
|
||||||
|
|
||||||
existing_repos = repos[:]
|
existing_repos = repos[:]
|
||||||
@ -48,7 +57,9 @@ class SVN_OT_repo_add(Operator, ImportHelper):
|
|||||||
repo = prefs.init_repo(context, path)
|
repo = prefs.init_repo(context, path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report(
|
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)
|
print(e)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
if not repo:
|
if not repo:
|
||||||
@ -65,6 +76,7 @@ class SVN_OT_repo_add(Operator, ImportHelper):
|
|||||||
|
|
||||||
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'}
|
||||||
|
|
||||||
bl_idname = "svn.repo_remove"
|
bl_idname = "svn.repo_remove"
|
||||||
@ -93,19 +105,98 @@ class SVN_MT_add_repo(Menu):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator(
|
layout.operator(
|
||||||
"svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER')
|
"svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER'
|
||||||
layout.operator("svn.checkout_initiate",
|
)
|
||||||
text="Create New Checkout", icon='URL').create = True
|
layout.operator(
|
||||||
|
"svn.checkout_initiate", text="Create New Checkout", icon='URL'
|
||||||
|
).create = True
|
||||||
|
|
||||||
|
|
||||||
def draw_prefs(self, context):
|
def draw_repo_list(self, context) -> None:
|
||||||
if self.checkout_mode:
|
layout = self.layout
|
||||||
draw_prefs_checkout(self, context)
|
|
||||||
else:
|
auth_in_progress = False
|
||||||
draw_prefs_repos(self, context)
|
auth_error = False
|
||||||
|
auth_proc = Processes.get('Authenticate')
|
||||||
|
if auth_proc:
|
||||||
|
auth_in_progress = auth_proc.is_running
|
||||||
|
auth_error = auth_proc.error
|
||||||
|
|
||||||
|
repo_col = layout.column()
|
||||||
|
split = repo_col.row().split()
|
||||||
|
split.row().label(text="SVN Repositories:")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
list_row = repo_col.row()
|
||||||
|
col = list_row.column()
|
||||||
|
col.template_list(
|
||||||
|
"SVN_UL_repositories",
|
||||||
|
"svn_repo_list",
|
||||||
|
self,
|
||||||
|
"repositories",
|
||||||
|
self,
|
||||||
|
"active_repo_idx",
|
||||||
|
)
|
||||||
|
|
||||||
|
op_col = list_row.column()
|
||||||
|
op_col.menu('SVN_MT_add_repo', icon='ADD', text="")
|
||||||
|
op_col.operator('svn.repo_remove', icon='REMOVE', text="")
|
||||||
|
|
||||||
|
if len(self.repositories) == 0:
|
||||||
|
return
|
||||||
|
if self.active_repo_idx - 1 > len(self.repositories):
|
||||||
|
return
|
||||||
|
if not self.active_repo:
|
||||||
|
return
|
||||||
|
|
||||||
|
repo_col.prop(self.active_repo, 'display_name', icon='FILE_TEXT')
|
||||||
|
repo_col.prop(self.active_repo, 'url', icon='URL')
|
||||||
|
repo_col.prop(self.active_repo, 'username', icon='USER')
|
||||||
|
repo_col.prop(self.active_repo, 'password', icon='LOCKED')
|
||||||
|
|
||||||
|
draw_process_info(context, layout.row())
|
||||||
|
|
||||||
|
if not self.active_repo.dir_exists:
|
||||||
|
draw_repo_error(layout, "Repository not found on file system.")
|
||||||
|
return
|
||||||
|
if not self.active_repo.is_valid_svn:
|
||||||
|
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
|
||||||
|
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.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self.repositories) > 0 and self.active_repo.authenticated:
|
||||||
|
layout.separator()
|
||||||
|
layout.label(text="SVN Files: ")
|
||||||
|
draw_file_list(context, layout)
|
||||||
|
|
||||||
|
if is_log_useful(context):
|
||||||
|
layout.separator()
|
||||||
|
layout.label(text="Revision History: ")
|
||||||
|
draw_svn_log(context, layout)
|
||||||
|
|
||||||
|
|
||||||
def draw_prefs_checkout(self, context):
|
def draw_repo_error(layout, message):
|
||||||
|
split = layout.split(factor=0.24)
|
||||||
|
split.row()
|
||||||
|
col = split.column()
|
||||||
|
col.alert = True
|
||||||
|
col.label(text=message, icon='ERROR')
|
||||||
|
|
||||||
|
|
||||||
|
def draw_checkout(self, context):
|
||||||
def get_terminal_howto():
|
def get_terminal_howto():
|
||||||
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."
|
||||||
@ -128,16 +219,22 @@ def draw_prefs_checkout(self, context):
|
|||||||
col.label(text=get_terminal_howto())
|
col.label(text=get_terminal_howto())
|
||||||
col.separator()
|
col.separator()
|
||||||
col.label(
|
col.label(
|
||||||
text="Downloading a repository can take a long time, and the UI will be locked.")
|
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="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(
|
col.label(
|
||||||
text="To interrupt the checkout, you can press Ctrl+C in the terminal.", icon='INFO')
|
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="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)
|
||||||
@ -150,7 +247,8 @@ def draw_prefs_checkout(self, context):
|
|||||||
row = col.row()
|
row = col.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.label(
|
row.label(
|
||||||
text="A repository at this filepath is already specified.", icon='ERROR')
|
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')
|
||||||
@ -163,7 +261,8 @@ def draw_prefs_checkout(self, context):
|
|||||||
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(
|
sub.label(
|
||||||
text="If you're sure you want to checkout another copy of the repo, feel free to proceed.")
|
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')
|
||||||
@ -173,101 +272,4 @@ def draw_prefs_checkout(self, context):
|
|||||||
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:
|
registry = [SVN_UL_repositories, SVN_OT_repo_add, SVN_OT_repo_remove, SVN_MT_add_repo]
|
||||||
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')
|
|
||||||
if auth_proc:
|
|
||||||
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:")
|
|
||||||
row = split.row()
|
|
||||||
row.alignment = 'RIGHT'
|
|
||||||
row.prop(self, 'debug_mode')
|
|
||||||
|
|
||||||
repo_col.enabled = not auth_in_progress
|
|
||||||
|
|
||||||
list_row = repo_col.row()
|
|
||||||
col = list_row.column()
|
|
||||||
col.template_list(
|
|
||||||
"SVN_UL_repositories",
|
|
||||||
"svn_repo_list",
|
|
||||||
self,
|
|
||||||
"repositories",
|
|
||||||
self,
|
|
||||||
"active_repo_idx",
|
|
||||||
)
|
|
||||||
|
|
||||||
op_col = list_row.column()
|
|
||||||
op_col.menu('SVN_MT_add_repo', icon='ADD', text="")
|
|
||||||
op_col.operator('svn.repo_remove', icon='REMOVE', text="")
|
|
||||||
|
|
||||||
if len(self.repositories) == 0:
|
|
||||||
return
|
|
||||||
if self.active_repo_idx-1 > len(self.repositories):
|
|
||||||
return
|
|
||||||
if not self.active_repo:
|
|
||||||
return
|
|
||||||
|
|
||||||
repo_col.prop(self.active_repo, 'display_name', icon='FILE_TEXT')
|
|
||||||
repo_col.prop(self.active_repo, 'url', icon='URL')
|
|
||||||
repo_col.prop(self.active_repo, 'username', icon='USER')
|
|
||||||
repo_col.prop(self.active_repo, 'password', icon='LOCKED')
|
|
||||||
|
|
||||||
draw_process_info(context, layout.row())
|
|
||||||
|
|
||||||
if not self.active_repo.dir_exists:
|
|
||||||
draw_repo_error(layout, "Repository not found on file system.")
|
|
||||||
return
|
|
||||||
if not self.active_repo.is_valid_svn:
|
|
||||||
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
|
|
||||||
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.")
|
|
||||||
return
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if is_log_useful(context):
|
|
||||||
layout.separator()
|
|
||||||
layout.label(text="Log: ")
|
|
||||||
draw_svn_log(context, layout, file_browser=False)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_repo_error(layout, message):
|
|
||||||
split = layout.split(factor=0.24)
|
|
||||||
split.row()
|
|
||||||
col = split.column()
|
|
||||||
col.alert = True
|
|
||||||
col.label(text=message, icon='ERROR')
|
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
|
||||||
SVN_UL_repositories,
|
|
||||||
SVN_OT_repo_add,
|
|
||||||
SVN_OT_repo_remove,
|
|
||||||
SVN_MT_add_repo
|
|
||||||
]
|
|
@ -4,7 +4,7 @@
|
|||||||
from bpy.types import Panel
|
from bpy.types import Panel
|
||||||
|
|
||||||
from ..util import get_addon_prefs
|
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):
|
class VIEW3D_PT_svn_credentials(Panel):
|
||||||
@ -16,12 +16,7 @@ class VIEW3D_PT_svn_credentials(Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
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
|
return repo and not repo.authenticated
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
@ -58,9 +53,8 @@ class VIEW3D_PT_svn_files(Panel):
|
|||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
repo = context.scene.svn.get_repo(context)
|
|
||||||
draw_process_info(context, layout)
|
draw_process_info(context, layout)
|
||||||
draw_repo_file_list(context, layout, repo)
|
draw_file_list(context, layout)
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
|
Loading…
Reference in New Issue
Block a user