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,
svn_commit,
svn_update,
ui_operators
ui_operators,
svn_checkout
)
modules = [
simple_commands,
svn_commit,
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 ..util import get_addon_prefs, redraw_viewport
# TODO: Add an operator to revert all local changes to the working copy.
class SVN_Operator:
@staticmethod
@ -53,8 +52,6 @@ class SVN_Operator_Single_File(SVN_Operator):
file = self.get_file(context)
if file:
Processes.start('Status')
# self.set_predicted_file_status(repo, file)
# file.status_prediction_type = "SKIP_ONCE"
redraw_viewport()
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
self.idx_updating = True
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_idx = self.repositories.find(scene_svn.svn_directory)
if scene_svn_idx == -1:
@ -62,13 +62,19 @@ class SVN_addon_preferences(AddonPreferences):
self.idx_updating = False
def update_ui_mode(self, context):
if self.ui_mode == 'CURRENT_BLEND':
def update_active_repo_mode(self, context):
if self.active_repo_mode == 'CURRENT_BLEND':
scene_svn = context.scene.svn
scene_svn_idx = self.repositories.find(scene_svn.svn_directory)
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",
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 = [
@ -76,7 +82,7 @@ class SVN_addon_preferences(AddonPreferences):
('SELECTED_REPO', "Selected Repo", "Communicate with the selected repository")
],
default = 'CURRENT_BLEND',
update = update_ui_mode
update = update_active_repo_mode
)
active_repo_idx: IntProperty(

View File

@ -36,7 +36,7 @@ class SVN_scene_properties(PropertyGroup):
"""
prefs = get_addon_prefs(context)
if prefs.ui_mode == 'CURRENT_BLEND':
if prefs.active_repo_mode == 'CURRENT_BLEND':
return self.get_scene_repo(context)
else:
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",
update=update_directory
)
@property
def is_valid(self):
def dir_exists(self):
dir_path = Path(self.directory)
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
if username:
self.username = username
if password:
self.password = password
if self.directory != directory:
# Don't set this if it's already set, to avoid infinite recursion
# via the update callback.
@ -252,10 +269,6 @@ class SVN_repository(PropertyGroup):
return self
@property
def exists(self) -> bool:
return Path(self.directory).exists()
### Credentials. ###
def update_cred(self, context):
if not (self.username and self.password):
@ -270,7 +283,7 @@ class SVN_repository(PropertyGroup):
def authenticate(self, context):
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')
# Trigger the file list filtering.
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.
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:
self.acquire_output(context, prefs)
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")
try:
if repo.is_valid:
if repo.is_valid_svn:
return execute_command(repo.directory, command)
except subprocess.CalledProcessError as error:
if ignore_errors:

View File

@ -150,7 +150,7 @@ class BGP_SVN_Log(BackgroundProcess):
print_errors=False,
use_cred=True
)
self.debug_print("Output: \n" + self.output)
self.debug_print("Output: \n" + str(self.output))
except subprocess.CalledProcessError as error:
error_msg = error.stderr.decode()
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.auth_failed = False
if prefs.ui_mode == 'CURRENT_BLEND':
if prefs.active_repo_mode == 'CURRENT_BLEND':
if not bpy.data.filepath:
scene_svn.svn_url = ""
return
@ -172,7 +172,7 @@ class BGP_SVN_Authenticate(BGP_SVN_Status):
def acquire_output(self, context, prefs):
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
super().acquire_output(context, prefs)

View File

@ -89,13 +89,12 @@ class SVN_UL_log(UIList):
def is_log_useful(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
active_file = repo.active_file
if active_file.status in ['unversioned', 'added']:
return False
if repo.file_search_filter:
any_visible = any([file.show_in_filelist for file in repo.external_files])
if not any_visible:
return False

View File

@ -3,13 +3,14 @@
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 ..util import get_addon_prefs
from .ui_log import draw_svn_log, is_log_useful
from .ui_file_list import draw_repo_file_list, draw_process_info
from ..threaded.background_process import Processes
import platform
class SVN_UL_repositories(UIList):
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()
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.label(text=repo.display_name)
if not repo.is_valid:
if not repo.dir_exists:
row.alert = True
row.prop(repo, 'directory', text="")
@ -80,13 +81,90 @@ class SVN_OT_repo_remove(Operator):
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_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
row = layout.row()
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_error = False
@ -95,7 +173,7 @@ def draw_prefs(self, context) -> None:
auth_in_progress = auth_proc.is_running
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.row()
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.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="")
if len(self.repositories) == 0:
@ -140,9 +218,15 @@ def draw_prefs(self, context) -> None:
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.")
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
@ -167,5 +251,6 @@ def draw_repo_error(layout, message):
registry = [
SVN_UL_repositories,
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
def poll(cls, 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)
else:
repo = context.scene.svn.get_repo(context)