2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from . import (
simple_commands,
svn_commit,
svn_update,
ui_operators,
svn_checkout
)
modules = [
simple_commands,
svn_commit,
svn_update,
ui_operators,
svn_checkout
]
@@ -0,0 +1,503 @@
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List, Dict, Union, Any, Set, Optional, Tuple
from pathlib import Path
import bpy
from bpy.props import StringProperty, IntProperty, EnumProperty, BoolProperty
from bpy.types import Context, Operator
from send2trash import send2trash
from ..threaded.execute_subprocess import execute_svn_command
from ..threaded.background_process import Processes
from ..util import get_addon_prefs, redraw_viewport
class SVN_Operator:
@staticmethod
def update_file_list(context):
repo = context.scene.svn.get_repo(context)
repo.refresh_ui_lists(context)
def execute_svn_command(self, context, command: List[str], use_cred=False) -> str:
# Since a status update might already be being requested when an SVN operator is run,
# we want to ignore the first update after any SVN operator.
# Otherwise it can result in a predicted state being overwritten by an outdated state.
# For example, the Commit operator sets a file to "Normal" state, then the old svn status
# arrives and sets it back to "Modified" state, which it isn't anymore.
return execute_svn_command(context, command, use_cred=use_cred)
class SVN_Operator_Single_File(SVN_Operator):
"""Base class for SVN operators operating on a single file."""
file_rel_path: StringProperty()
# Flag to differentiate operators that require that the file exists pre-execute.
missing_file_allowed = False
def execute(self, context: Context) -> Set[str]:
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.
self.report(
{'ERROR'},
f'File is no longer on the file system: "{self.file_rel_path}"',
)
return {'CANCELLED'}
status = Processes.get('Status')
if status:
Processes.kill('Status')
ret = self._execute(context)
file = self.get_file(context)
Processes.start('Status')
redraw_viewport()
self.update_file_list(context)
return ret
def _execute(self, context: Context) -> Set[str]:
raise NotImplementedError
def get_file_full_path(self, context) -> Path:
repo = context.scene.svn.get_repo(context)
return Path.joinpath(Path(repo.directory), Path(self.file_rel_path))
def get_file(self, context) -> "SVN_file":
repo = context.scene.svn.get_repo(context)
return repo.external_files.get(self.file_rel_path)
def file_exists(self, context) -> bool:
exists = self.get_file_full_path(context).exists()
if not exists and not type(self).missing_file_allowed:
self.report({'INFO'}, "File was not found, cancelling.")
return exists
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
return
class Popup_Operator:
popup_width = 400
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(
self, width=type(self).popup_width
)
class Warning_Operator(Popup_Operator):
def draw(self, context):
layout = self.layout.column(align=True)
warning = self.get_warning_text(context)
for line in warning.split("\n"):
row = layout.row()
row.alert = True
row.label(text=line)
def get_warning_text(self, context):
raise NotImplemented
class May_Modifiy_Current_Blend(SVN_Operator_Single_File, Warning_Operator):
def file_is_current_blend(self, context) -> bool:
current_blend = context.scene.svn.get_repo(context).current_blend_file
return current_blend and current_blend.svn_path == self.file_rel_path
set_outdated_flag = True
reload_file: BoolProperty(
name="Reload File (Keep UI)",
description="Reload the file after the operation is completed. The UI layout will be preserved",
default=False,
)
def invoke(self, context, event):
self.reload_file = False
if self.file_is_current_blend(context) or self.get_warning_text(context):
return context.window_manager.invoke_props_dialog(self, width=500)
return self.execute(context)
def get_warning_text(self, context):
if self.file_is_current_blend(context):
return "This will modify the currently opened .blend file."
return ""
def draw(self, context):
super().draw(context)
if self.file_is_current_blend(context):
self.layout.prop(self, 'reload_file')
def execute(self, context):
super().execute(context)
if self.reload_file:
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False)
elif self.file_is_current_blend(context) and self.set_outdated_flag:
context.scene.svn.file_is_outdated = True
return {'FINISHED'}
class SVN_OT_update_single(May_Modifiy_Current_Blend, Operator):
bl_idname = "svn.update_single"
bl_label = "Update File"
bl_description = (
"Download the latest available version of this file from the remote repository"
)
bl_options = {'INTERNAL'}
missing_file_allowed = True
def _execute(self, context: Context) -> Set[str]:
self.will_conflict = False
repo = context.scene.svn.get_repo(context)
file_entry = repo.external_files.get(self.file_rel_path)
if file_entry.status not in ['normal', 'none']:
self.will_conflict = True
self.execute_svn_command(
context,
["svn", "up", f"{self.file_rel_path}", "--accept", "postpone"],
use_cred=True,
)
self.report({'INFO'}, f'Updated "{self.file_rel_path}" to the latest version.')
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
if self.will_conflict:
file_entry.status = 'conflicted'
else:
file_entry.status = 'normal'
file_entry.repos_status = 'none'
return {"FINISHED"}
class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
bl_idname = "svn.download_file_revision"
bl_label = "Download Revision"
bl_description = "Download this revision of this file"
bl_options = {'INTERNAL'}
missing_file_allowed = True
revision: IntProperty(default=0)
def invoke(self, context, event):
repo = context.scene.svn.get_repo(context)
file_entry = repo.external_files.get(self.file_rel_path)
if self.file_is_current_blend(context) and file_entry.status != 'normal':
self.report(
{'ERROR'}, 'You must first revert or commit the changes to this file.'
)
return {'CANCELLED'}
return super().invoke(context, event)
def _execute(self, context: Context) -> Set[str]:
repo = context.scene.svn.get_repo(context)
file_entry = repo.external_files.get(self.file_rel_path)
if file_entry.status == 'modified':
# If file has local modifications, let's avoid a conflict by cancelling
# and telling the user to resolve it in advance.
self.report(
{'ERROR'},
"Cancelled: You have local modifications to this file. You must revert or commit it first!",
)
return {'CANCELLED'}
self.svn_download_file_revision(context, self.file_rel_path, self.revision)
self.report(
{'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}"
)
return {"FINISHED"}
def svn_download_file_revision(self, context, svn_file_path: str, revision=0):
commands = ["svn", "up", f"{self.file_rel_path}", "--accept", "postpone"]
if self.revision > 0:
commands.insert(2, f"-r{self.revision}")
self.execute_svn_command(context, commands, use_cred=True)
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
file_entry['revision'] = self.revision
latest_rev = repo.get_latest_revision_of_file(self.file_rel_path)
if latest_rev == self.revision:
file_entry.status = 'normal'
file_entry.repos_status = 'none'
else:
file_entry.status = 'normal'
file_entry.repos_status = 'modified'
class SVN_OT_restore_file(May_Modifiy_Current_Blend, Operator):
bl_idname = "svn.restore_file"
bl_label = "Restore File"
bl_description = "Restore this deleted file to its previously checked out revision"
bl_options = {'INTERNAL'}
missing_file_allowed = True
def svn_revert(self, context, svn_file_path):
self.execute_svn_command(context, ["svn", "revert", f"{svn_file_path}"])
def _execute(self, context: Context) -> Set[str]:
self.svn_revert(context, self.file_rel_path)
return {"FINISHED"}
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
file_entry.status = 'normal'
class SVN_OT_revert_file(SVN_OT_restore_file):
bl_idname = "svn.revert_file"
bl_label = "Revert File"
bl_description = "PERMANENTLY DISCARD local changes to this file and return it to the state of the last local revision. Cannot be undone"
bl_options = {'INTERNAL'}
missing_file_allowed = False
def get_warning_text(self, context) -> str:
return (
"You will irreversibly and permanently lose the changes you've made to this file:\n "
+ self.file_rel_path
)
class SVN_OT_revert_and_update(SVN_OT_download_file_revision, SVN_OT_revert_file):
"""Convenience operator for the "This file is outdated" warning message. Normally, these two operations should be done separately!"""
bl_idname = "svn.revert_and_update_file"
bl_label = "Revert And Update File"
bl_description = "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):
bl_idname = "svn.add_file"
bl_label = "Add File"
bl_description = (
"Mark this file for addition to the remote repository. It can then be committed"
)
bl_options = {'INTERNAL'}
def _execute(self, context: Context) -> Set[str]:
result = self.execute_svn_command(
context, ["svn", "add", f"{self.file_rel_path}"]
)
if result:
f = self.get_file(context)
return {"FINISHED"}
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
file_entry.status = 'added'
class SVN_OT_unadd_file(SVN_Operator_Single_File, Operator):
bl_idname = "svn.unadd_file"
bl_label = "Un-Add File"
bl_description = "Un-mark this file as being added to the remote repository. It will not be committed"
bl_options = {'INTERNAL'}
def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command(
context, ["svn", "rm", "--keep-local", f"{self.file_rel_path}"]
)
return {"FINISHED"}
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
file_entry.status = 'unversioned'
class SVN_OT_trash_file(SVN_Operator_Single_File, Warning_Operator, Operator):
bl_idname = "svn.trash_file"
bl_label = "Trash File"
bl_description = "Move this file to the recycle bin"
bl_options = {'INTERNAL'}
file_rel_path: StringProperty()
missing_file_allowed = False
def get_warning_text(self, context):
return (
"Are you sure you want to move this file to the recycle bin?\n "
+ self.file_rel_path
)
def _execute(self, context: Context) -> Set[str]:
send2trash([self.get_file_full_path(context)])
f = self.get_file(context)
# Since this operator is only available for Unversioned files,
# we want to remove the file entry when removing the file.
context.scene.svn.get_repo(context).remove_file_entry(f)
return {"FINISHED"}
class SVN_OT_remove_file(SVN_Operator_Single_File, Warning_Operator, Operator):
bl_idname = "svn.remove_file"
bl_label = "Remove File"
bl_description = "Mark this file for removal from the remote repository"
bl_options = {'INTERNAL'}
missing_file_allowed = True
def get_warning_text(self, context):
return (
"This file will be deleted for everyone:\n "
+ self.file_rel_path
+ "\nAre you sure?"
)
def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command(context, ["svn", "remove", f"{self.file_rel_path}"])
return {"FINISHED"}
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
file_entry.status = 'deleted'
class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator):
bl_idname = "svn.resolve_conflict"
bl_label = "Resolve Conflict"
bl_description = "Resolve a conflict, by discarding either local or remote changes"
bl_options = {'INTERNAL'}
set_outdated_flag = False
resolve_method: EnumProperty(
name="Resolve Method",
description="Method to use to resolve the conflict",
items=[
(
'mine-full',
'Keep Mine',
'Overwrite the new changes downloaded from the remote, and keep the local changes instead',
),
(
'theirs-full',
'Keep Theirs',
'Overwrite the local changes with those downloaded from the remote',
),
],
)
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width=500)
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.alert = True
col.label(text="Choose which version of the file to keep.")
col.row().prop(self, 'resolve_method', expand=True)
col.separator()
if self.resolve_method == 'mine-full':
col.label(text="Local changes will be kept.")
col.label(
text="When committing, the changes someone else made will be overwritten."
)
else:
col.label(text="Local changes will be permanently lost.")
super().draw(context)
def _execute(self, context: Context) -> Set[str]:
self.execute_svn_command(
context,
[
"svn",
"resolve",
f"{self.file_rel_path}",
"--accept",
f"{self.resolve_method}",
],
)
if self.file_is_current_blend(context):
# If user wants to keep their changes to the current file,
# remove the outdated file warning UI.
context.scene.svn.file_is_outdated = self.resolve_method != 'mine-full'
return {"FINISHED"}
def set_predicted_file_status(self, repo, file_entry: "SVN_file"):
if self.resolve_method == 'mine-full':
file_entry.status = 'modified'
else:
file_entry.status = 'normal'
class SVN_OT_cleanup(SVN_Operator, Operator):
bl_idname = "svn.cleanup"
bl_label = "SVN Cleanup"
bl_description = "Resolve issues that can arise from previous SVN processes having been interrupted"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
# Don't allow attempting to cleanup while Update/Commit is running.
return not get_addon_prefs(context).is_busy
def execute(self, context: Context) -> Set[str]:
repo = context.scene.svn.get_repo(context)
repo.external_files.clear()
self.execute_svn_command(context, ["svn", "cleanup"])
repo.reload_svn_log(context)
Processes.kill('Commit')
Processes.kill('Update')
Processes.kill('Authenticate')
Processes.kill('Activate File')
Processes.restart('Status')
Processes.restart('Log')
Processes.restart('Redraw Viewport')
self.report({'INFO'}, "SVN Cleanup complete.")
return {"FINISHED"}
registry = [
SVN_OT_update_single,
SVN_OT_revert_and_update,
SVN_OT_revert_file,
SVN_OT_restore_file,
SVN_OT_download_file_revision,
SVN_OT_add_file,
SVN_OT_unadd_file,
SVN_OT_trash_file,
SVN_OT_remove_file,
SVN_OT_resolve_conflict,
SVN_OT_cleanup,
]
@@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List, Dict, Union, Any, Set, Optional, Tuple
from bpy.types import Operator
from bpy.props import BoolProperty
from .simple_commands import SVN_Operator
from ..util import get_addon_prefs
from ..threaded.background_process import Processes
import subprocess
from pathlib import Path
class SVN_OT_checkout_initiate(Operator):
bl_idname = "svn.checkout_initiate"
bl_label = "Initiate SVN Checkout"
bl_description = "Checkout a remote SVN repository"
bl_options = {'INTERNAL'}
create: BoolProperty(
name="Create Repo Entry",
description="Whether a new repo entry should be created, or the active one used",
default=True
)
def execute(self, context):
prefs = get_addon_prefs(context)
if self.create:
prefs.repositories.add()
prefs.active_repo_idx = len(prefs.repositories)-1
prefs.checkout_mode = True
return {'FINISHED'}
class SVN_OT_checkout_finalize(Operator, SVN_Operator):
bl_idname = "svn.checkout_finalize"
bl_label = "Finalize SVN Checkout"
bl_description = "Checkout the specified SVN repository to the selected path"
bl_options = {'INTERNAL'}
def execute(self, context):
prefs = get_addon_prefs(context)
repo = prefs.active_repo
# `svn checkout` is an outlier in every way from other SVN commands:
# - Credentials are provided with an equal sign
# - We need live output in the console, but we don't need to store it.
# - It needs to be able to run even if the current directory isn't a valid repo.
# So, we're not going to use our `execute_subprocess` api here.
self.execute_svn_command(
context,
['svn', 'cleanup']
)
p = subprocess.Popen(
["svn", "checkout", f"--username={repo.username}",
f"--password={repo.password}", repo.url, repo.display_name],
shell=False,
cwd=repo.directory+"/",
stdout=subprocess.PIPE,
start_new_session=True
)
repo.directory = str((Path(repo.directory) / repo.display_name))
while True:
line = p.stdout.readline().decode()
print(line.replace("\n", ""))
if not line:
break
prefs = get_addon_prefs(context)
prefs.checkout_mode = False
prefs.save_repo_info_to_file()
Processes.start('Authenticate')
return {'FINISHED'}
class SVN_OT_checkout_cancel(Operator):
bl_idname = "svn.checkout_cancel"
bl_label = "Cancel SVN Checkout"
bl_description = "Cancel the checkout UI"
bl_options = {'INTERNAL'}
def execute(self, context):
prefs = get_addon_prefs(context)
prefs.checkout_mode = False
repo = prefs.active_repo
if not repo.url and not repo.username and not repo.password and not repo.directory:
prefs.repositories.remove(prefs.active_repo_idx)
return {'FINISHED'}
registry = [
SVN_OT_checkout_initiate,
SVN_OT_checkout_finalize,
SVN_OT_checkout_cancel
]
@@ -0,0 +1,234 @@
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import List, Dict, Union, Any, Set, Optional, Tuple
import bpy
from bpy.types import PropertyGroup, Operator, Context
from bpy.props import StringProperty
from ..threaded.background_process import Processes
from .simple_commands import SVN_Operator, Popup_Operator
from ..util import get_addon_prefs
# Store a reference to the running operator in global namespace when it runs,
# so that its sub-operators can mess
active_commit_operator = None
class SVN_commit_line(PropertyGroup):
"""Property Group representing a single line of a commit message.
Only needed for UI/UX purpose, so we can store the commit message
even if the user changes their mind about wanting to commit."""
def update_line(self, context):
line_entries = context.scene.svn.get_repo(context).commit_lines
for i, line_entry in enumerate(line_entries):
if line_entry == self and i >= len(line_entries)-2:
# The last line was just modified
if self.line:
# Content was added to the last line - add another line.
line_entries.add()
line: StringProperty(update=update_line)
class SVN_OT_commit(SVN_Operator, Popup_Operator, Operator):
bl_idname = "svn.commit"
bl_label = "SVN Commit"
bl_description = "Commit a selection of files to the remote repository"
bl_options = {'INTERNAL'}
bl_property = "first_line" # Focus the text input box
popup_width = 600
# The first line of the commit message needs to be an operator property in order
# for us to be able to focus the input box automatically when the window pops up
# (see bl_property above)
def update_first_line(self, context):
repo = context.scene.svn.get_repo(context)
repo.commit_lines[0].line = self.first_line
first_line: StringProperty(
name="First Line",
description="First line of the commit message",
update=update_first_line
)
@staticmethod
def get_committable_files(context) -> List["SVN_file"]:
"""Return the list of file entries whose status allows committing"""
repo = context.scene.svn.get_repo(context)
if not repo:
return
svn_file_list = repo.external_files
committable_statuses = ['modified', 'added', 'deleted']
files_to_commit = [
f for f in svn_file_list if f.status in committable_statuses]
return files_to_commit
@classmethod
def poll(cls, context):
if get_addon_prefs(context).is_busy:
# Don't allow attempting to Update/Commit while either is still running.
return False
return cls.get_committable_files(context)
def invoke(self, context, event):
repo = context.scene.svn.get_repo(context)
if repo.commit_message == "":
repo.commit_message = ""
global active_commit_operator
active_commit_operator = self
self.first_line = repo.commit_lines[0].line
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
# when we change the operator's checkboxes or
self.is_file_dirty_on_invoke = bpy.data.is_dirty
for f in repo.external_files:
f.include_in_commit = False
for f in self.get_committable_files(context):
if not f.will_conflict:
f.include_in_commit = True
return super().invoke(context, event)
def draw(self, context):
"""Draws the boolean toggle list with a list of strings for the button texts."""
layout = self.layout
files = self.get_committable_files(context)
layout.label(
text="These files will be pushed to the remote repository:")
repo = context.scene.svn.get_repo(context)
row = layout.row()
row.label(text="Filename")
row.label(text="Status")
for file in files:
row = layout.row()
split = row.split()
checkbox_ui = split.row()
status_ui = split.row()
checkbox_ui.prop(file, "include_in_commit", text=file.file_name)
text = file.status_name
icon = file.status_icon
if file.will_conflict:
# We don't want to conflict-resolve during a commit, it's
# confusing. User should resolve this as a separate step.
checkbox_ui.enabled = False
text = "Conflicting"
status_ui.alert = True
icon = 'ERROR'
elif file == repo.current_blend_file and self.is_file_really_dirty:
split = status_ui.split(factor=0.7)
status_ui = split.row()
status_ui.alert = True
text += " but not saved!"
icon = 'ERROR'
op_row = split.row()
op_row.alignment = 'LEFT'
op_row.operator('svn.save_during_commit',
icon='FILE_BLEND', text="Save")
status_ui.label(text=text, icon=icon)
row = layout.row()
row.label(text="Commit message:")
# Draw input box for first line, which is special because we want it to
# get focused automatically for smooth UX. (see `bl_property` above)
row = layout.row()
row.prop(self, 'first_line', text="")
row.operator(SVN_OT_commit_msg_clear.bl_idname, text="", icon='TRASH')
for i in range(1, len(repo.commit_lines)):
# Draw input boxes until the last one that has text, plus two, minimum three.
# Why two after the last line? Because then you can use Tab to go to the next line.
# Why at least 3 lines? Because then you can write a one-liner without
# the OK button jumping away.
layout.prop(
repo.commit_lines[i], 'line', index=i, text="")
continue
def execute(self, context: Context) -> Set[str]:
committable_files = self.get_committable_files(context)
files_to_commit = [f for f in committable_files if f.include_in_commit]
repo = context.scene.svn.get_repo(context)
if not files_to_commit:
self.report({'ERROR'},
"No files were selected, nothing to commit.")
return {'CANCELLED'}
if len(repo.commit_message) < 2:
self.report({'ERROR'},
"Please describe your changes in the commit message.")
return {'CANCELLED'}
filepaths = [f.svn_path for f in files_to_commit]
self.set_predicted_file_statuses(files_to_commit)
Processes.stop('Status')
Processes.start('Commit',
commit_msg=repo.commit_message,
file_list=filepaths
)
report = f"{(len(files_to_commit))} files"
if len(files_to_commit) == 1:
report = files_to_commit[0].svn_path
self.report({'INFO'},
f"Started committing {report}. See console for when it's finished.")
return {"FINISHED"}
def set_predicted_file_statuses(self, file_entries):
for f in file_entries:
if f.status != 'deleted':
if f.repos_status == 'none':
# We modified the file, and it was not modified on the repo,
# predict the status to be "normal".
f.status = 'normal'
else:
# If we modified the file, but it was modified on the repo:
f.status = 'conflicted'
# TODO: What happens if we DID delete the file, AND it was modified on the repo?
# Should probably also predict a conflict.
f.status_prediction_type = "SVN_COMMIT"
class SVN_OT_commit_save_file(Operator):
bl_idname = "svn.save_during_commit"
bl_label = "Save During SVN Commit"
bl_description = "Save During SVN Commit"
bl_options = {'INTERNAL'}
def execute(self, context):
global active_commit_operator
active_commit_operator.is_file_really_dirty = False
bpy.ops.wm.save_mainfile()
return {'FINISHED'}
class SVN_OT_commit_msg_clear(Operator):
bl_idname = "svn.clear_commit_message"
bl_label = "Clear SVN Commit Message"
bl_description = "Clear the commit message"
bl_options = {'INTERNAL'}
def execute(self, context):
context.scene.svn.get_repo(context).commit_message = ""
global active_commit_operator
active_commit_operator.first_line = ""
return {'FINISHED'}
registry = [
SVN_OT_commit,
SVN_OT_commit_save_file,
SVN_OT_commit_msg_clear,
SVN_commit_line
]
@@ -0,0 +1,96 @@
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Set
import bpy
from bpy.types import Operator, Context
from bpy.props import IntProperty
from .simple_commands import May_Modifiy_Current_Blend
from ..threaded.background_process import Processes
from ..util import get_addon_prefs
class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
bl_idname = "svn.update_all"
bl_label = "SVN Update All"
bl_description = "Download all the latest updates from the remote repository"
bl_options = {'INTERNAL'}
revision: IntProperty(
name="Revision",
description="Which revision to revert the repository to. 0 means to update to the latest version instead",
default=0
)
@classmethod
def poll(cls, context):
if get_addon_prefs(context).is_busy:
# Don't allow attempting to Update/Commit while either is still running.
return False
repo = context.scene.svn.get_repo(context)
if not repo:
return False
for f in repo.external_files:
if f.repos_status != 'none':
return True
return True
def invoke(self, context, event):
repo = context.scene.svn.get_repo(context)
current_blend = repo.current_blend_file
if self.revision == 0:
if current_blend and current_blend.repos_status != 'none':
# If the current file will be modified, warn user.
self.file_rel_path = current_blend.svn_path
return context.window_manager.invoke_props_dialog(self, width=500)
else:
for f in repo.external_files:
if f.status in ['modified', 'added', 'conflicted', 'deleted', 'missing', 'unversioned']:
# If user wants to check out an older version of the repo but
# there are uncommitted local changes to any files, warn user.
return context.window_manager.invoke_props_dialog(self, width=500)
return self.execute(context)
def draw(self, context):
if self.revision != 0:
layout = self.layout
col = layout.column()
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(
text="past point in time, you would get a better result if you reverted or committed your changes first.")
col.separator()
col.label(
text="Press OK to proceed anyways. Click out of this window to cancel.")
super().draw(context)
def execute(self, context: Context) -> Set[str]:
Processes.stop('Status')
if self.reload_file:
current_file = context.scene.svn.get_repo(context).current_blend_file
command = ["svn", "up", current_file.svn_path, "--accept", "postpone"]
if self.revision > 0:
command.insert(2, f"-r{self.revision}")
self.execute_svn_command(
context,
command,
use_cred=True
)
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath, load_ui=False)
Processes.start('Log')
Processes.start('Update', revision=self.revision)
return {"FINISHED"}
registry = [
SVN_OT_update_all,
]
@@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from ..threaded.background_process import Processes
class SVN_OT_custom_tooltip(Operator):
"""Tooltip"""
bl_idname = "svn.custom_tooltip"
bl_label = ""
bl_description = " "
bl_options = {'INTERNAL'}
tooltip: StringProperty(
name="Tooltip",
description="Tooltip that is displayed when mouse hovering this operator"
)
copy_on_click: BoolProperty(
name="Copy on Click",
description="If True, the tooltip will be copied to the clipboard when the operator is clicked",
default=False
)
@classmethod
def description(cls, context, properties):
tooltip = properties.tooltip
if properties.copy_on_click:
tooltip = "Copy to clipboard: " + properties.tooltip
return tooltip
def execute(self, context):
if self.copy_on_click:
context.window_manager.clipboard = self.tooltip
self.report({'INFO'}, "Copied to Clipboard: " + self.tooltip)
return {'FINISHED'}
class SVN_OT_clear_error(Operator):
bl_idname = "svn.clear_error"
bl_label = "Error:"
bl_description = ""
bl_options = {'INTERNAL'}
process_id: StringProperty()
@classmethod
def description(cls, context, properties):
process = Processes.get(properties.process_id)
if not process:
return "Process doesn't exist: " + properties.process_id
return process.error_description + "\n\n" + process.error + "\n\n Click to clear the error and copy it to your clipboard"
def execute(self, context):
process = Processes.get(self.process_id)
if not process:
self.report({'WARNING'}, f'Process not found: "{self.process_id}"')
return {'FINISHED'}
context.window_manager.clipboard = process.error_description + "\n\n" + process.error
if process.repeat_delay > 0:
process.start()
else:
process.error = ""
process.output = ""
self.report({'INFO'}, "Copied error to Clipboard.")
return {'FINISHED'}
registry = [
SVN_OT_custom_tooltip,
SVN_OT_clear_error
]