2025-12-01
This commit is contained in:
@@ -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
|
||||
]
|
||||
Reference in New Issue
Block a user