235 lines
8.7 KiB
Python
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
|
|
]
|