SVN: Checkout, Multi-Repo, Optimizations & Clean-up #104

Merged
Demeter Dzadik merged 12 commits from Mets/blender-studio-pipeline:SVN-improvements into main 2023-07-10 16:49:03 +02:00
13 changed files with 236 additions and 37 deletions
Showing only changes of commit 42c5015f12 - Show all commits

View File

@ -2,12 +2,14 @@ from . import (
simple_commands, simple_commands,
svn_commit, svn_commit,
svn_update, svn_update,
ui_operators ui_operators,
svn_checkout
) )
modules = [ modules = [
simple_commands, simple_commands,
svn_commit, svn_commit,
svn_update, svn_update,
ui_operators ui_operators,
svn_checkout
] ]

View File

@ -15,7 +15,6 @@ from ..threaded.execute_subprocess import execute_svn_command
from ..threaded.background_process import Processes from ..threaded.background_process import Processes
from ..util import get_addon_prefs, redraw_viewport from ..util import get_addon_prefs, redraw_viewport
# TODO: Add an operator to revert all local changes to the working copy.
class SVN_Operator: class SVN_Operator:
@staticmethod @staticmethod
@ -53,8 +52,6 @@ class SVN_Operator_Single_File(SVN_Operator):
file = self.get_file(context) file = self.get_file(context)
if file: if file:
Processes.start('Status') Processes.start('Status')
# self.set_predicted_file_status(repo, file)
# file.status_prediction_type = "SKIP_ONCE"
redraw_viewport() redraw_viewport()
self.update_file_list(context) self.update_file_list(context)

View File

@ -0,0 +1,94 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# (c) 2022, Blender Foundation - Demeter Dzadik
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)
prefs.active_repo_mode = 'SELECTED_REPO'
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
]

View File

@ -47,7 +47,7 @@ class SVN_addon_preferences(AddonPreferences):
return return
self.idx_updating = True self.idx_updating = True
active_repo = self.active_repo active_repo = self.active_repo
if self.ui_mode == 'CURRENT_BLEND': if self.active_repo_mode == 'CURRENT_BLEND':
scene_svn = context.scene.svn scene_svn = context.scene.svn
scene_svn_idx = self.repositories.find(scene_svn.svn_directory) scene_svn_idx = self.repositories.find(scene_svn.svn_directory)
if scene_svn_idx == -1: if scene_svn_idx == -1:
@ -62,13 +62,19 @@ class SVN_addon_preferences(AddonPreferences):
self.idx_updating = False self.idx_updating = False
def update_ui_mode(self, context): def update_active_repo_mode(self, context):
if self.ui_mode == 'CURRENT_BLEND': if self.active_repo_mode == 'CURRENT_BLEND':
scene_svn = context.scene.svn scene_svn = context.scene.svn
scene_svn_idx = self.repositories.find(scene_svn.svn_directory) scene_svn_idx = self.repositories.find(scene_svn.svn_directory)
self.active_repo_idx = scene_svn_idx self.active_repo_idx = scene_svn_idx
ui_mode: EnumProperty( checkout_mode: BoolProperty(
name="Checkout In Progress",
description="Internal flag to indicate that the user is currently trying to create a new checkout",
default=False
)
active_repo_mode: EnumProperty(
name = "Choose Repository", name = "Choose Repository",
description = "Whether the add-on should communicate with the repository of the currently opened .blend file, or the repository selected in the list below", description = "Whether the add-on should communicate with the repository of the currently opened .blend file, or the repository selected in the list below",
items = [ items = [
@ -76,7 +82,7 @@ class SVN_addon_preferences(AddonPreferences):
('SELECTED_REPO', "Selected Repo", "Communicate with the selected repository") ('SELECTED_REPO', "Selected Repo", "Communicate with the selected repository")
], ],
default = 'CURRENT_BLEND', default = 'CURRENT_BLEND',
update = update_ui_mode update = update_active_repo_mode
) )
active_repo_idx: IntProperty( active_repo_idx: IntProperty(

View File

@ -36,7 +36,7 @@ class SVN_scene_properties(PropertyGroup):
""" """
prefs = get_addon_prefs(context) prefs = get_addon_prefs(context)
if prefs.ui_mode == 'CURRENT_BLEND': if prefs.active_repo_mode == 'CURRENT_BLEND':
return self.get_scene_repo(context) return self.get_scene_repo(context)
else: else:
return prefs.active_repo return prefs.active_repo

View File

@ -234,13 +234,30 @@ class SVN_repository(PropertyGroup):
description="Absolute directory path of the SVN repository's root in the file system", description="Absolute directory path of the SVN repository's root in the file system",
update=update_directory update=update_directory
) )
@property @property
def is_valid(self): def dir_exists(self):
dir_path = Path(self.directory) dir_path = Path(self.directory)
return dir_path.exists() and dir_path.is_dir() return dir_path.exists() and dir_path.is_dir()
def initialize(self, directory: str, url: str, display_name=""): @property
def is_valid_svn(self):
dir_path = Path(self.directory)
root_dir, base_url = get_svn_info(self.directory)
return (
dir_path.exists() and
dir_path.is_dir() and
root_dir and base_url and
root_dir == self.directory and
base_url == self.url
)
def initialize(self, directory: str, url: str, display_name="", username="", password=""):
self.url = url self.url = url
if username:
self.username = username
if password:
self.password = password
if self.directory != directory: if self.directory != directory:
# Don't set this if it's already set, to avoid infinite recursion # Don't set this if it's already set, to avoid infinite recursion
# via the update callback. # via the update callback.
@ -252,10 +269,6 @@ class SVN_repository(PropertyGroup):
return self return self
@property
def exists(self) -> bool:
return Path(self.directory).exists()
### Credentials. ### ### Credentials. ###
def update_cred(self, context): def update_cred(self, context):
if not (self.username and self.password): if not (self.username and self.password):
@ -270,7 +283,7 @@ class SVN_repository(PropertyGroup):
def authenticate(self, context): def authenticate(self, context):
self.auth_failed = False self.auth_failed = False
if self.exists and self.is_cred_entered: if self.is_valid_svn and self.is_cred_entered:
Processes.start('Authenticate') Processes.start('Authenticate')
# Trigger the file list filtering. # Trigger the file list filtering.
self.file_search_filter = self.file_search_filter self.file_search_filter = self.file_search_filter

View File

@ -67,6 +67,9 @@ class BackgroundProcess:
Should save data into self.output and self.error. Should save data into self.output and self.error.
Reading Blender data from this function is safe, but writing isn't! Reading Blender data from this function is safe, but writing isn't!
""" """
repo = context.scene.svn.get_repo(context)
if not repo.is_valid_svn:
self.stop()
try: try:
self.acquire_output(context, prefs) self.acquire_output(context, prefs)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:

View File

@ -34,7 +34,7 @@ def execute_svn_command(context, command: List[str], *, ignore_errors=False, pri
command.append("--non-interactive") command.append("--non-interactive")
try: try:
if repo.is_valid: if repo.is_valid_svn:
return execute_command(repo.directory, command) return execute_command(repo.directory, command)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
if ignore_errors: if ignore_errors:

View File

@ -150,7 +150,7 @@ class BGP_SVN_Log(BackgroundProcess):
print_errors=False, print_errors=False,
use_cred=True use_cred=True
) )
self.debug_print("Output: \n" + self.output) self.debug_print("Output: \n" + str(self.output))
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
error_msg = error.stderr.decode() error_msg = error.stderr.decode()
if "No such revision" in error_msg: if "No such revision" in error_msg:

View File

@ -77,7 +77,7 @@ def init_svn_of_current_file(_scene=None):
repo.authenticated = False repo.authenticated = False
repo.auth_failed = False repo.auth_failed = False
if prefs.ui_mode == 'CURRENT_BLEND': if prefs.active_repo_mode == 'CURRENT_BLEND':
if not bpy.data.filepath: if not bpy.data.filepath:
scene_svn.svn_url = "" scene_svn.svn_url = ""
return return
@ -172,7 +172,7 @@ class BGP_SVN_Authenticate(BGP_SVN_Status):
def acquire_output(self, context, prefs): def acquire_output(self, context, prefs):
repo = context.scene.svn.get_repo(context) repo = context.scene.svn.get_repo(context)
if not repo or not repo.is_cred_entered or repo.authenticated: if not repo or not repo.is_valid_svn or not repo.is_cred_entered or repo.authenticated:
return return
super().acquire_output(context, prefs) super().acquire_output(context, prefs)

View File

@ -89,16 +89,15 @@ class SVN_UL_log(UIList):
def is_log_useful(context): def is_log_useful(context):
repo = context.scene.svn.get_repo(context) repo = context.scene.svn.get_repo(context)
if len(repo.log) == 0: if len(repo.log) == 0 or len(repo.external_files) == 0:
return False return False
active_file = repo.active_file active_file = repo.active_file
if active_file.status in ['unversioned', 'added']: if active_file.status in ['unversioned', 'added']:
return False return False
if repo.file_search_filter: any_visible = any([file.show_in_filelist for file in repo.external_files])
any_visible = any([file.show_in_filelist for file in repo.external_files]) if not any_visible:
if not any_visible: return False
return False
return True return True

View File

@ -3,13 +3,14 @@
from pathlib import Path from pathlib import Path
from bpy.types import UIList, Operator from bpy.types import UIList, Operator, Menu
from bpy_extras.io_utils import ImportHelper from bpy_extras.io_utils import ImportHelper
from ..util import get_addon_prefs from ..util import get_addon_prefs
from .ui_log import draw_svn_log, is_log_useful from .ui_log import draw_svn_log, is_log_useful
from .ui_file_list import draw_repo_file_list, draw_process_info from .ui_file_list import draw_repo_file_list, draw_process_info
from ..threaded.background_process import Processes from ..threaded.background_process import Processes
import platform
class SVN_UL_repositories(UIList): class SVN_UL_repositories(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@ -17,11 +18,11 @@ class SVN_UL_repositories(UIList):
row = layout.row() row = layout.row()
prefs = get_addon_prefs(context) prefs = get_addon_prefs(context)
if prefs.ui_mode == 'CURRENT_BLEND' and repo != context.scene.svn.get_repo(context): if prefs.active_repo_mode == 'CURRENT_BLEND' and repo != context.scene.svn.get_repo(context):
row.enabled = False row.enabled = False
row.label(text=repo.display_name) row.label(text=repo.display_name)
if not repo.is_valid: if not repo.dir_exists:
row.alert = True row.alert = True
row.prop(repo, 'directory', text="") row.prop(repo, 'directory', text="")
@ -80,13 +81,90 @@ class SVN_OT_repo_remove(Operator):
prefs.save_repo_info_to_file() prefs.save_repo_info_to_file()
return {'FINISHED'} return {'FINISHED'}
class SVN_MT_add_repo(Menu):
bl_idname = "SVN_MT_add_repo"
bl_label = "Add Repo"
def draw_prefs(self, context) -> None: 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_prefs(self, context):
if self.checkout_mode:
draw_prefs_checkout(self, context)
else:
draw_prefs_repos(self, context)
def draw_prefs_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")
def draw_prefs_repos(self, context) -> None:
layout = self.layout layout = self.layout
row = layout.row() row = layout.row()
row.use_property_split = True row.use_property_split = True
row.prop(self, 'ui_mode', expand=True) row.prop(self, 'active_repo_mode', expand=True)
auth_in_progress = False auth_in_progress = False
auth_error = False auth_error = False
@ -95,7 +173,7 @@ def draw_prefs(self, context) -> None:
auth_in_progress = auth_proc.is_running auth_in_progress = auth_proc.is_running
auth_error = auth_proc.error auth_error = auth_proc.error
if self.ui_mode == 'CURRENT_BLEND' and not context.scene.svn.get_repo(context): if self.active_repo_mode == 'CURRENT_BLEND' and not context.scene.svn.get_repo(context):
split = layout.split(factor=0.4) split = layout.split(factor=0.4)
split.row() split.row()
split.row().label(text="Current file is not in a repository.") split.row().label(text="Current file is not in a repository.")
@ -122,7 +200,7 @@ def draw_prefs(self, context) -> None:
) )
op_col = list_row.column() op_col = list_row.column()
op_col.operator('svn.repo_add', icon='ADD', text="") op_col.menu('SVN_MT_add_repo', icon='ADD', text="")
op_col.operator('svn.repo_remove', icon='REMOVE', text="") op_col.operator('svn.repo_remove', icon='REMOVE', text="")
if len(self.repositories) == 0: if len(self.repositories) == 0:
@ -140,9 +218,15 @@ def draw_prefs(self, context) -> None:
draw_process_info(context, layout.row()) draw_process_info(context, layout.row())
if not self.active_repo.exists: if not self.active_repo.dir_exists:
draw_repo_error(layout, "Repository not found on file system.") draw_repo_error(layout, "Repository not found on file system.")
return 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: 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.") draw_repo_error(layout, "Repository not authenticated. Enter your credentials.")
return return
@ -167,5 +251,6 @@ def draw_repo_error(layout, message):
registry = [ registry = [
SVN_UL_repositories, SVN_UL_repositories,
SVN_OT_repo_add, SVN_OT_repo_add,
SVN_OT_repo_remove SVN_OT_repo_remove,
SVN_MT_add_repo
] ]

View File

@ -17,7 +17,7 @@ class VIEW3D_PT_svn_credentials(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
prefs = get_addon_prefs(context) prefs = get_addon_prefs(context)
if prefs.ui_mode == 'CURRENT_BLEND': if prefs.active_repo_mode == 'CURRENT_BLEND':
repo = context.scene.svn.get_scene_repo(context) repo = context.scene.svn.get_scene_repo(context)
else: else:
repo = context.scene.svn.get_repo(context) repo = context.scene.svn.get_repo(context)