2025-12-01
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from . import (
|
||||
ui_file_list,
|
||||
ui_sidebar,
|
||||
ui_filebrowser,
|
||||
ui_log,
|
||||
ui_repo_list,
|
||||
ui_outdated_warning,
|
||||
ui_context_menus
|
||||
)
|
||||
|
||||
modules = [
|
||||
ui_file_list,
|
||||
ui_sidebar,
|
||||
ui_filebrowser,
|
||||
ui_log,
|
||||
ui_repo_list,
|
||||
ui_outdated_warning,
|
||||
ui_context_menus
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, UIList, Operator
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_context_match(context: Context, uilayout_type: str, bl_idname: str) -> bool:
|
||||
"""For example, when right-clicking on a UIList, the uilayout_type will
|
||||
be `ui_list` and the bl_idname is that of the UIList being right-clicked.
|
||||
"""
|
||||
uilayout = getattr(context, uilayout_type, None)
|
||||
return uilayout and uilayout.bl_idname == bl_idname
|
||||
|
||||
|
||||
def svn_file_list_context_menu(self: UIList, context: Context) -> None:
|
||||
if not check_context_match(context, 'ui_list', 'SVN_UL_file_list'):
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
active_file = repo.active_file
|
||||
file_abs_path = repo.get_file_abspath(active_file)
|
||||
if active_file.name.endswith("blend"):
|
||||
op = layout.operator("wm.open_mainfile",
|
||||
text=f"Open {active_file.name}")
|
||||
op.filepath = str(file_abs_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 = str(file_abs_path)
|
||||
op.display_file_selector = False
|
||||
op.load_ui = False
|
||||
|
||||
else:
|
||||
layout.operator("wm.path_open",
|
||||
text=f"Open {active_file.name}").filepath = str(file_abs_path)
|
||||
layout.operator("wm.path_open",
|
||||
text=f"Open Containing Folder").filepath = Path(file_abs_path).parent.as_posix()
|
||||
layout.separator()
|
||||
|
||||
|
||||
def svn_log_list_context_menu(self: UIList, context: Context) -> None:
|
||||
if not check_context_match(context, 'ui_list', 'SVN_UL_log'):
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
active_log = repo.active_log
|
||||
layout.operator("svn.update_all",
|
||||
text=f"Revert Repository To r{active_log.revision_number}").revision = active_log.revision_number
|
||||
layout.separator()
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.UI_MT_list_item_context_menu.append(svn_file_list_context_menu)
|
||||
bpy.types.UI_MT_list_item_context_menu.append(svn_log_list_context_menu)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.UI_MT_list_item_context_menu.remove(svn_file_list_context_menu)
|
||||
bpy.types.UI_MT_list_item_context_menu.remove(svn_log_list_context_menu)
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import UIList
|
||||
from bpy.props import BoolProperty
|
||||
|
||||
from .. import constants
|
||||
from ..util import get_addon_prefs, dots
|
||||
from ..threaded.background_process import Processes
|
||||
|
||||
|
||||
class SVN_UL_file_list(UIList):
|
||||
# Value that indicates that this item has passed the filter process successfully. See rna_ui.c.
|
||||
UILST_FLT_ITEM = 1 << 30
|
||||
|
||||
show_file_paths: BoolProperty(
|
||||
name="Show File Paths",
|
||||
description="Show file paths relative to the SVN root, instead of just the file name"
|
||||
)
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
# As long as there are any items, always draw the filters.
|
||||
self.use_filter_show = True
|
||||
|
||||
if self.layout_type != 'DEFAULT':
|
||||
raise NotImplemented
|
||||
|
||||
file_entry = item
|
||||
prefs = get_addon_prefs(context)
|
||||
|
||||
main_row = layout.row()
|
||||
split = main_row.split(factor=0.6)
|
||||
filepath_ui = split.row()
|
||||
split = split.split(factor=0.4)
|
||||
status_ui = split.row(align=True)
|
||||
|
||||
ops_ui = split.row(align=True)
|
||||
ops_ui.alignment = 'RIGHT'
|
||||
|
||||
ops_ui.enabled = file_entry.status_prediction_type == 'NONE' and not prefs.is_busy
|
||||
|
||||
if self.show_file_paths:
|
||||
filepath_ui.prop(file_entry, 'name', text="",
|
||||
emboss=False, icon=file_entry.file_icon)
|
||||
else:
|
||||
filepath_ui.label(text=file_entry.file_name, icon=file_entry.file_icon)
|
||||
|
||||
statuses = [file_entry.status]
|
||||
# SVN operations
|
||||
ops = []
|
||||
if file_entry.status in ['missing', 'deleted']:
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.restore_file', text="", icon='LOOP_BACK'))
|
||||
if file_entry.status == 'missing':
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.remove_file', text="", icon='TRASH'))
|
||||
elif file_entry.status == 'added':
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.unadd_file', text="", icon='REMOVE'))
|
||||
elif file_entry.status == 'unversioned':
|
||||
ops.append(ops_ui.operator('svn.add_file', text="", icon='ADD'))
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.trash_file', text="", icon='TRASH'))
|
||||
|
||||
elif file_entry.status == 'modified':
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.revert_file', text="", icon='LOOP_BACK'))
|
||||
if file_entry.repos_status == 'modified':
|
||||
# The file isn't actually `conflicted` yet, by SVN's definition,
|
||||
# but it will be as soon as we try to commit or update.
|
||||
# I think it's better to let the user know in advance.
|
||||
statuses.append('conflicted')
|
||||
# Updating the file will create an actual conflict.
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.update_single', text="", icon='IMPORT'))
|
||||
|
||||
elif file_entry.status == 'conflicted':
|
||||
ops.append(ops_ui.operator('svn.resolve_conflict',
|
||||
text="", icon='TRACKING_CLEAR_FORWARDS'))
|
||||
elif file_entry.status in ['incomplete', 'obstructed']:
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.cleanup', text="", icon='BRUSH_DATA'))
|
||||
elif file_entry.status == 'none':
|
||||
if file_entry.repos_status == 'added':
|
||||
# From user POV it makes a bit more sense to call a file that doesn't
|
||||
# exist yet "added" instead of "outdated".
|
||||
statuses.append('added')
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.update_single', text="", icon='IMPORT'))
|
||||
elif file_entry.status == 'normal' and file_entry.repos_status == 'modified':
|
||||
# From user POV, this file is outdated, not 'normal'.
|
||||
statuses = ['none']
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.update_single', text="", icon='IMPORT'))
|
||||
elif file_entry.status in ['normal', 'external', 'ignored']:
|
||||
pass
|
||||
else:
|
||||
print("Unknown file status: ", file_entry.svn_path,
|
||||
file_entry.status, file_entry.repos_status)
|
||||
|
||||
for op in ops:
|
||||
if hasattr(op, 'file_rel_path'):
|
||||
op.file_rel_path = file_entry.svn_path
|
||||
|
||||
# Populate the status icons.
|
||||
for status in statuses:
|
||||
icon = constants.SVN_STATUS_DATA[status][0]
|
||||
explainer = status_ui.operator(
|
||||
'svn.explain_status', text="", icon=icon, emboss=False)
|
||||
explainer.status = status
|
||||
explainer.file_rel_path = file_entry.svn_path
|
||||
|
||||
@classmethod
|
||||
def cls_filter_items(cls, context, data, propname):
|
||||
"""By moving all of this logic to a classmethod (and all the filter
|
||||
properties to the addon preferences) we can find a visible entry
|
||||
from other UI code, allowing us to avoid situations where the active
|
||||
element becomes hidden."""
|
||||
flt_neworder = []
|
||||
list_items = getattr(data, propname)
|
||||
flt_flags = [file.show_in_filelist *
|
||||
cls.UILST_FLT_ITEM for file in list_items]
|
||||
|
||||
helper_funcs = bpy.types.UI_UL_list
|
||||
|
||||
# This list should ALWAYS be sorted alphabetically.
|
||||
flt_neworder = helper_funcs.sort_items_by_name(list_items, "name")
|
||||
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
if not repo:
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
return type(self).cls_filter_items(context, data, propname)
|
||||
|
||||
def draw_filter(self, context, layout):
|
||||
"""Custom filtering UI.
|
||||
Toggles are stored in addon preferences, see cls_filter_items().
|
||||
"""
|
||||
main_row = layout.row()
|
||||
row = main_row.row(align=True)
|
||||
|
||||
row.prop(self, 'show_file_paths', text="",
|
||||
toggle=True, icon="FILE_FOLDER")
|
||||
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
if repo:
|
||||
row.prop(repo, 'file_search_filter', text="")
|
||||
|
||||
|
||||
def draw_process_info(context, layout):
|
||||
prefs = get_addon_prefs(context)
|
||||
process_message = ""
|
||||
any_error = False
|
||||
col = layout.column()
|
||||
for process in Processes.processes.values():
|
||||
if process.name not in {'Commit', 'Update', 'Log', 'Status', 'Authenticate'}:
|
||||
continue
|
||||
|
||||
if process.error:
|
||||
row = col.row()
|
||||
row.alert = True
|
||||
warning = row.operator(
|
||||
'svn.clear_error', text=f"SVN {process.name}: Error Occurred. Hover to view", icon='ERROR')
|
||||
warning.process_id = process.name
|
||||
any_error = True
|
||||
break
|
||||
|
||||
if process.is_running:
|
||||
message = process.get_ui_message(context)
|
||||
if message:
|
||||
message = message.replace("...", dots())
|
||||
process_message = f"SVN: {message}"
|
||||
|
||||
if not any_error and process_message:
|
||||
col.label(text=process_message)
|
||||
if prefs.debug_mode:
|
||||
col.label(text="Processes: " +
|
||||
", ".join([p.name for p in Processes.running_processes]))
|
||||
|
||||
|
||||
def draw_file_list(context, layout):
|
||||
prefs = get_addon_prefs(context)
|
||||
repo = prefs.active_repo
|
||||
if not repo:
|
||||
return
|
||||
|
||||
if not repo.authenticated:
|
||||
row = layout.row()
|
||||
row.alert=True
|
||||
row.label(text="Repository is not authenticated.", icon='ERROR')
|
||||
return
|
||||
|
||||
main_col = layout.column()
|
||||
main_row = main_col.row()
|
||||
split = main_row.split(factor=0.6)
|
||||
filepath_row = split.row()
|
||||
filepath_row.label(text=" Filepath")
|
||||
|
||||
status_row = split.row()
|
||||
status_row.label(text=" Status")
|
||||
|
||||
ops_row = main_row.row()
|
||||
ops_row.alignment = 'RIGHT'
|
||||
ops_row.label(text="Operations")
|
||||
|
||||
row = main_col.row()
|
||||
row.template_list(
|
||||
"SVN_UL_file_list",
|
||||
"svn_file_list",
|
||||
repo,
|
||||
"external_files",
|
||||
repo,
|
||||
"external_files_active_index",
|
||||
)
|
||||
|
||||
col = row.column()
|
||||
|
||||
col.separator()
|
||||
col.operator("svn.commit", icon='EXPORT', text="")
|
||||
col.operator("svn.update_all", icon='IMPORT', text="").revision = 0
|
||||
|
||||
col.separator()
|
||||
col.operator("svn.cleanup", icon='BRUSH_DATA', text="")
|
||||
|
||||
|
||||
registry = [
|
||||
SVN_UL_file_list,
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Panel
|
||||
from bl_ui.space_filebrowser import FileBrowserPanel
|
||||
|
||||
from .ui_log import draw_svn_log, is_log_useful
|
||||
from .ui_file_list import draw_file_list
|
||||
from ..util import get_addon_prefs
|
||||
|
||||
class FILEBROWSER_PT_SVN_files(FileBrowserPanel, Panel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOLS'
|
||||
bl_category = "Bookmarks"
|
||||
bl_label = "SVN Files"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
|
||||
prefs = get_addon_prefs(context)
|
||||
return prefs.active_repo and prefs.active_repo.authenticated
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
draw_file_list(context, layout)
|
||||
|
||||
|
||||
class FILEBROWSER_PT_SVN_log(FileBrowserPanel, Panel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOLS'
|
||||
bl_category = "Bookmarks"
|
||||
bl_parent_id = "FILEBROWSER_PT_SVN_files"
|
||||
bl_label = "Revision History"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not super().poll(context):
|
||||
return False
|
||||
|
||||
return is_log_useful(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
draw_svn_log(context, layout)
|
||||
|
||||
|
||||
registry = [
|
||||
FILEBROWSER_PT_SVN_files,
|
||||
FILEBROWSER_PT_SVN_log
|
||||
]
|
||||
@@ -0,0 +1,279 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.props import IntProperty, BoolProperty
|
||||
from bpy.types import UIList, Panel, Operator
|
||||
from ..util import get_addon_prefs
|
||||
|
||||
|
||||
class SVN_UL_log(UIList):
|
||||
show_all_logs: BoolProperty(
|
||||
name='Show All Logs',
|
||||
description='Show the complete SVN Log, instead of only entries that affected the currently selected file',
|
||||
default=False
|
||||
)
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if self.layout_type != 'DEFAULT':
|
||||
raise NotImplemented
|
||||
|
||||
svn = data
|
||||
log_entry = item
|
||||
|
||||
num, auth, date, msg = layout_log_split(layout.row())
|
||||
|
||||
active_file = svn.active_file
|
||||
num.label(text=str(log_entry.revision_number))
|
||||
if item.revision_number == active_file.revision:
|
||||
num.operator('svn.tooltip_log', text="", icon='LAYER_ACTIVE',
|
||||
emboss=False).log_rev = log_entry.revision_number
|
||||
elif log_entry.changes_file(active_file):
|
||||
get_older = num.operator(
|
||||
'svn.download_file_revision', text="", icon='IMPORT', emboss=False)
|
||||
get_older.revision = log_entry.revision_number
|
||||
get_older.file_rel_path = active_file.svn_path
|
||||
auth.label(text=log_entry.revision_author)
|
||||
date.label(text=log_entry.revision_date_simple)
|
||||
|
||||
commit_msg = log_entry.commit_message
|
||||
commit_msg = commit_msg.split(
|
||||
"\n")[0] if "\n" in commit_msg else commit_msg
|
||||
commit_msg = commit_msg[:50] + \
|
||||
"..." if len(commit_msg) > 52 else commit_msg
|
||||
msg.alignment = 'LEFT'
|
||||
msg.operator("svn.display_commit_message", text=commit_msg,
|
||||
emboss=False).log_rev = log_entry.revision_number
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
"""Custom filtering functionality:
|
||||
- Always sort by descending revision number
|
||||
- Allow searching for various criteria
|
||||
"""
|
||||
svn = data
|
||||
log_entries = getattr(data, propname)
|
||||
|
||||
# Start off with all entries flagged as visible.
|
||||
flt_flags = [self.bitflag_filter_item] * len(log_entries)
|
||||
# Always sort by descending revision number
|
||||
flt_neworder = sorted(range(len(log_entries)),
|
||||
key=lambda i: log_entries[i].revision_number)
|
||||
flt_neworder.reverse()
|
||||
|
||||
if not self.show_all_logs:
|
||||
flt_flags = [
|
||||
log_entry.affects_active_file * self.bitflag_filter_item
|
||||
for log_entry in log_entries
|
||||
]
|
||||
|
||||
if self.filter_name:
|
||||
# Filtering: Allow comma-separated keywords.
|
||||
# ALL keywords must be found somewhere in the log entry for it to show up.
|
||||
filter_words = [word.strip().lower()
|
||||
for word in self.filter_name.split(",")]
|
||||
for idx, log_entry in enumerate(log_entries):
|
||||
for filter_word in filter_words:
|
||||
if filter_word not in log_entry.text_to_search:
|
||||
flt_flags[idx] = 0
|
||||
break
|
||||
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
def draw_filter(self, context, layout):
|
||||
"""Custom filtering UI.
|
||||
"""
|
||||
main_row = layout.row()
|
||||
main_row.prop(self, 'filter_name', text="")
|
||||
main_row.prop(self, 'show_all_logs', text="",
|
||||
toggle=True, icon='ALIGN_JUSTIFY')
|
||||
|
||||
|
||||
def is_log_useful(context) -> bool:
|
||||
"""Return whether the log has any useful info to display."""
|
||||
|
||||
prefs = get_addon_prefs(context)
|
||||
repo = prefs.active_repo
|
||||
|
||||
if not repo or not repo.authenticated:
|
||||
return False
|
||||
|
||||
if len(repo.log) == 0 or len(repo.external_files) == 0:
|
||||
return False
|
||||
active_file = repo.active_file
|
||||
if active_file.status in ['unversioned', 'added']:
|
||||
return False
|
||||
|
||||
any_visible = any([file.show_in_filelist for file in repo.external_files])
|
||||
if not any_visible:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class VIEW3D_PT_svn_log(Panel):
|
||||
"""Display the revision history of the selected file."""
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'SVN'
|
||||
bl_label = 'Revision History'
|
||||
bl_parent_id = "VIEW3D_PT_svn_files"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return is_log_useful(context)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
draw_svn_log(context, layout)
|
||||
|
||||
|
||||
def layout_log_split(layout):
|
||||
main = layout.split(factor=0.4)
|
||||
num_and_auth = main.row()
|
||||
date_and_msg = main.row()
|
||||
|
||||
num_and_auth_split = num_and_auth.split(factor=0.5)
|
||||
num = num_and_auth_split.row()
|
||||
auth = num_and_auth_split.row()
|
||||
|
||||
date_and_msg_split = date_and_msg.split(factor=0.3)
|
||||
date = date_and_msg_split.row()
|
||||
msg = date_and_msg_split.row()
|
||||
|
||||
return num, auth, date, msg
|
||||
|
||||
|
||||
def draw_svn_log(context, layout):
|
||||
num, auth, date, msg = layout_log_split(layout.row())
|
||||
num.label(text="Rev. #")
|
||||
auth.label(text="Author")
|
||||
date.label(text="Date")
|
||||
msg.label(text="Message")
|
||||
|
||||
prefs = get_addon_prefs(context)
|
||||
repo = prefs.active_repo
|
||||
layout.template_list(
|
||||
"SVN_UL_log",
|
||||
"svn_log",
|
||||
repo,
|
||||
"log",
|
||||
repo,
|
||||
"log_active_index",
|
||||
)
|
||||
|
||||
active_log = repo.active_log
|
||||
if not active_log:
|
||||
return
|
||||
layout.label(text="Revision Date: " + active_log.revision_date)
|
||||
|
||||
layout.label(
|
||||
text=f"Files changed in revision `r{active_log.revision_number}`:")
|
||||
|
||||
col = layout.column(align=True)
|
||||
row = col.row()
|
||||
split = row.split(factor=0.80)
|
||||
split.label(text=" Filepath")
|
||||
row = split.row()
|
||||
row.alignment = 'RIGHT'
|
||||
row.label(text="Action")
|
||||
for f in active_log.changed_files:
|
||||
row = col.row()
|
||||
split = row.split(factor=0.90)
|
||||
split.prop(f, 'name', emboss=False, text="", icon=f.file_icon)
|
||||
row = split.row()
|
||||
row.alignment = 'RIGHT'
|
||||
row.operator('svn.explain_status', text="",
|
||||
icon=f.status_icon, emboss=False).status = f.status
|
||||
|
||||
|
||||
def execute_tooltip_log(self, context):
|
||||
"""Set the index on click, to act as if this operator button was
|
||||
click-through in the UIList."""
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
tup = repo.get_log_by_revision(self.log_rev)
|
||||
if tup:
|
||||
repo.log_active_index = tup[0]
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SVN_OT_log_tooltip(Operator):
|
||||
bl_idname = "svn.tooltip_log"
|
||||
bl_label = "" # Don't want the first line of the tooltip on mouse hover.
|
||||
# bl_description = "An operator to be drawn in the log list, that can display a dynamic tooltip"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
log_rev: IntProperty(
|
||||
description="Revision number of the log entry to show in the tooltip"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return "This is the currently checked out version of the file"
|
||||
|
||||
execute = execute_tooltip_log
|
||||
|
||||
|
||||
class SVN_OT_log_show_commit_msg(Operator):
|
||||
bl_idname = "svn.display_commit_message"
|
||||
bl_label = "" # Don't want the first line of the tooltip on mouse hover.
|
||||
# bl_description = "Show the currently active commit, using a dynamic tooltip"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
log_rev: IntProperty(
|
||||
description="Revision number of the log entry to show in the tooltip"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
log_entry = context.scene.svn.get_repo(context).get_log_by_revision(properties.log_rev)[
|
||||
1]
|
||||
commit_msg = log_entry.commit_message
|
||||
|
||||
# Prettify the tooltips.
|
||||
pretty_msg = ""
|
||||
for line in commit_msg.split("\n"):
|
||||
# Remove leading/trailing whitespace
|
||||
line = line.strip()
|
||||
|
||||
# Add punctuation mark
|
||||
if not (line.endswith(".") or line.endswith("!") or line.endswith("?")):
|
||||
line = line + "."
|
||||
|
||||
# Split long lines into several
|
||||
limit = 300
|
||||
if len(line) > limit:
|
||||
words = line.split(" ")
|
||||
sub_lines = []
|
||||
|
||||
new_line = ""
|
||||
for word in words:
|
||||
if len(new_line) + len(word) < limit:
|
||||
new_line += " "+word
|
||||
else:
|
||||
sub_lines.append(new_line)
|
||||
new_line = word
|
||||
else:
|
||||
sub_lines.append(new_line)
|
||||
line = "\n".join(sub_lines)
|
||||
|
||||
pretty_msg += "\n"+line
|
||||
|
||||
# Remove last period because Blender adds it.
|
||||
if pretty_msg.endswith("."):
|
||||
pretty_msg = pretty_msg[:-1]
|
||||
|
||||
return pretty_msg
|
||||
|
||||
execute = execute_tooltip_log
|
||||
|
||||
|
||||
registry = [
|
||||
VIEW3D_PT_svn_log,
|
||||
SVN_UL_log,
|
||||
SVN_OT_log_tooltip,
|
||||
SVN_OT_log_show_commit_msg
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def draw_outdated_file_warning(self, context):
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
current_file = None
|
||||
if not repo:
|
||||
return
|
||||
try:
|
||||
current_file = repo.current_blend_file
|
||||
except ValueError:
|
||||
# This can happen if the svn_directory property wasn't updated yet (not enough time has passed since opening the file)
|
||||
pass
|
||||
if not current_file:
|
||||
# If the current file is not in an SVN repository.
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.alert = True
|
||||
|
||||
if current_file.status == 'conflicted':
|
||||
row.operator('svn.resolve_conflict',
|
||||
text="SVN: This .blend file is conflicted.", icon='ERROR')
|
||||
elif current_file.repos_status != 'none' or context.scene.svn.file_is_outdated:
|
||||
op = row.operator('svn.revert_and_update_file', text="SVN: This .blend file may be outdated.", icon='ERROR')
|
||||
op.file_rel_path = repo.current_blend_file.svn_path
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(draw_outdated_file_warning)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.TOPBAR_MT_editor_menus.remove(draw_outdated_file_warning)
|
||||
@@ -0,0 +1,276 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import platform
|
||||
|
||||
from bpy.types import UIList, Operator, Menu
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
|
||||
from ..util import get_addon_prefs
|
||||
from .ui_log import draw_svn_log, is_log_useful
|
||||
from .ui_file_list import draw_file_list, draw_process_info
|
||||
from ..threaded.background_process import Processes
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
class SVN_UL_repositories(UIList):
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon, active_data, active_propname
|
||||
):
|
||||
repo = item
|
||||
row = layout.row()
|
||||
|
||||
row.label(text=repo.display_name)
|
||||
|
||||
if not repo.dir_exists:
|
||||
row.alert = True
|
||||
row.prop(repo, 'directory', text="")
|
||||
|
||||
|
||||
class SVN_OT_repo_add(Operator, ImportHelper):
|
||||
"""Add a repository to the list"""
|
||||
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
bl_idname = "svn.repo_add"
|
||||
bl_label = "Add Repository"
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs(context)
|
||||
repos = prefs.repositories
|
||||
|
||||
path = Path(self.filepath)
|
||||
if not path.exists():
|
||||
# It's unlikely that a path that the user JUST BROWSED doesn't exist.
|
||||
# So, this actually happens when the user leaves a filename in the
|
||||
# file browser text box while trying to select the folder...
|
||||
# Basically, Blender is dumb, and it will add that filename to the
|
||||
# end of the browsed path. We need to discard that.
|
||||
path = path.parent
|
||||
if path.is_file():
|
||||
# Maybe the user actually did select an existing file in the repo.
|
||||
# We still want to discard the filename.
|
||||
path = path.parent
|
||||
|
||||
existing_repos = repos[:]
|
||||
try:
|
||||
repo = prefs.init_repo(context, path)
|
||||
except Exception as e:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
"Failed to initialize repository. Ensure you have SVN installed, and that the selected directory is the root of a repository.",
|
||||
)
|
||||
print(e)
|
||||
return {'CANCELLED'}
|
||||
if not repo:
|
||||
self.report({'ERROR'}, "Failed to initialize repository.")
|
||||
return {'CANCELLED'}
|
||||
if repo in existing_repos:
|
||||
self.report({'INFO'}, "Repository already present.")
|
||||
else:
|
||||
self.report({'INFO'}, "Repository added.")
|
||||
prefs.active_repo_idx = repos.find(repo.directory)
|
||||
prefs.save_repo_info_to_file()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SVN_OT_repo_remove(Operator):
|
||||
"""Remove a repository from the list"""
|
||||
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
bl_idname = "svn.repo_remove"
|
||||
bl_label = "Remove Repository"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return len(get_addon_prefs(context).repositories) > 0
|
||||
|
||||
def execute(self, context):
|
||||
prefs = get_addon_prefs(context)
|
||||
active_index = prefs.active_repo_idx
|
||||
repos = prefs.repositories
|
||||
|
||||
prefs.repositories.remove(prefs.active_repo_idx)
|
||||
to_index = min(active_index, len(repos) - 1)
|
||||
prefs.active_repo_idx = to_index
|
||||
prefs.save_repo_info_to_file()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SVN_MT_add_repo(Menu):
|
||||
bl_idname = "SVN_MT_add_repo"
|
||||
bl_label = "Add Repo"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(
|
||||
"svn.repo_add", text="Browse Existing Checkout", icon='FILE_FOLDER'
|
||||
)
|
||||
layout.operator(
|
||||
"svn.checkout_initiate", text="Create New Checkout", icon='URL'
|
||||
).create = True
|
||||
|
||||
|
||||
def draw_repo_list(self, context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
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
|
||||
|
||||
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_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():
|
||||
msg_windows = "If you don't, cancel this operation and toggle it using Window->Toggle System Console."
|
||||
msg_linux = "If you don't, quit Blender and re-launch it from a terminal."
|
||||
msg_mac = msg_linux
|
||||
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
return msg_windows
|
||||
elif system == "Linux":
|
||||
return msg_linux
|
||||
elif system == "Darwin":
|
||||
return msg_mac
|
||||
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.alert = True
|
||||
|
||||
col.label(text="IMPORTANT! ", icon='ERROR')
|
||||
col.label(text="Make sure you have Blender's terminal open!")
|
||||
col.label(text=get_terminal_howto())
|
||||
col.separator()
|
||||
col.label(
|
||||
text="Downloading a repository can take a long time, and the UI will be locked."
|
||||
)
|
||||
col.label(
|
||||
text="Without a terminal, you won't be able to track the progress of the checkout."
|
||||
)
|
||||
col.separator()
|
||||
|
||||
col = layout.column()
|
||||
col.label(
|
||||
text="To interrupt the checkout, you can press Ctrl+C in the terminal.",
|
||||
icon='INFO',
|
||||
)
|
||||
col.label(
|
||||
text="You can resume it by re-running this operation, or with the SVN Update button.",
|
||||
icon='INFO',
|
||||
)
|
||||
col.separator()
|
||||
|
||||
prefs = get_addon_prefs(context)
|
||||
repo = prefs.repositories[-1]
|
||||
col.prop(repo, 'directory')
|
||||
for other_repo in prefs.repositories:
|
||||
if other_repo == repo:
|
||||
continue
|
||||
if other_repo.directory == repo.directory:
|
||||
row = col.row()
|
||||
row.alert = True
|
||||
row.label(
|
||||
text="A repository at this filepath is already specified.", icon='ERROR'
|
||||
)
|
||||
break
|
||||
|
||||
col.prop(repo, 'display_name', text="Folder Name", icon='NEWFOLDER')
|
||||
col.prop(repo, 'url', icon='URL')
|
||||
for other_repo in prefs.repositories:
|
||||
if other_repo == repo:
|
||||
continue
|
||||
if other_repo.url == repo.url:
|
||||
sub = col.column()
|
||||
sub.alert = True
|
||||
sub.label(text="A repository with this URL is already specified.")
|
||||
sub.label(
|
||||
text="If you're sure you want to checkout another copy of the repo, feel free to proceed."
|
||||
)
|
||||
break
|
||||
col.prop(repo, 'username', icon='USER')
|
||||
col.prop(repo, 'password', icon='LOCKED')
|
||||
|
||||
op_row = layout.row()
|
||||
op_row.operator('svn.checkout_finalize', text="Checkout", icon='CHECKMARK')
|
||||
op_row.operator('svn.checkout_cancel', text="Cancel", icon="X")
|
||||
|
||||
|
||||
registry = [SVN_UL_repositories, SVN_OT_repo_add, SVN_OT_repo_remove, SVN_MT_add_repo]
|
||||
@@ -0,0 +1,63 @@
|
||||
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Panel
|
||||
|
||||
from .ui_file_list import draw_file_list, draw_process_info
|
||||
|
||||
|
||||
class VIEW3D_PT_svn_credentials(Panel):
|
||||
"""Prompt the user to enter their username and password for the remote repository of the current .blend file."""
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'SVN'
|
||||
bl_label = 'SVN Credentials'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
return repo and not repo.authenticated
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
col = layout.column(align=True)
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
row = col.row()
|
||||
row.prop(repo, 'display_name', text="Repo Name", icon='FILE_TEXT')
|
||||
url = row.operator('svn.custom_tooltip', text="", icon='URL')
|
||||
url.tooltip = repo.url
|
||||
url.copy_on_click = True
|
||||
col.prop(repo, 'username', icon='USER')
|
||||
col.prop(repo, 'password', icon='UNLOCKED')
|
||||
draw_process_info(context, layout)
|
||||
|
||||
|
||||
class VIEW3D_PT_svn_files(Panel):
|
||||
"""Display a list of files in the SVN repository of the current .blend file."""
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'SVN'
|
||||
bl_label = 'SVN Files'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
return repo and repo.authenticated
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
draw_process_info(context, layout)
|
||||
draw_file_list(context, layout)
|
||||
|
||||
|
||||
registry = [
|
||||
VIEW3D_PT_svn_credentials,
|
||||
VIEW3D_PT_svn_files,
|
||||
]
|
||||
Reference in New Issue
Block a user