Files
blender-portable-repo/scripts/addons/blender_svn/operators/svn_commit.py
T
2026-03-17 14:58:51 -06:00

235 lines
8.7 KiB
Python

# 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
]