SVN: Checkout, Multi-Repo, Optimizations & Clean-up #104
@ -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
|
||||
]
|
@ -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)
|
||||
|
94
scripts-blender/addons/blender_svn/operators/svn_checkout.py
Normal file
94
scripts-blender/addons/blender_svn/operators/svn_checkout.py
Normal 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
|
||||
]
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -89,16 +89,15 @@ 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
|
||||
any_visible = any([file.show_in_filelist for file in repo.external_files])
|
||||
if not any_visible:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
@ -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
|
||||
]
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user