191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
# SPDX-FileCopyrightText: 2022 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
import subprocess
|
|
|
|
from ..util import redraw_viewport
|
|
from .. import constants
|
|
from .execute_subprocess import execute_svn_command
|
|
from .background_process import BackgroundProcess
|
|
|
|
|
|
def reload_svn_log(self, context):
|
|
"""Read the svn.log file (written by this addon) into the log entry list."""
|
|
|
|
repo = self
|
|
repo.log.clear()
|
|
|
|
# Read file into lists of lines where each list is one log entry
|
|
filepath = self.log_file_path
|
|
if not filepath.exists():
|
|
# Nothing to read!
|
|
return
|
|
|
|
chunks = []
|
|
with open(filepath, 'r') as f:
|
|
next(f) # Skip the first line of dashes.
|
|
chunk = []
|
|
for line in f:
|
|
line = line.replace("\n", "")
|
|
if line == "-" * 72:
|
|
# Line of dashes indicates the log entry is over.
|
|
chunks.append(chunk)
|
|
chunk = []
|
|
continue
|
|
chunk.append(line)
|
|
|
|
previous_rev_number = 0
|
|
for chunk in chunks:
|
|
if not chunk[0]:
|
|
chunk.pop(0)
|
|
# Read the first line of the svn log containing revision number, author,
|
|
# date and commit message length.
|
|
r_number, r_author, r_date, r_msg_length = chunk[0].split(" | ")
|
|
r_number = int(r_number[1:])
|
|
if r_number != previous_rev_number+1:
|
|
# print(f"SVN: Warning: Revision order seems wrong at r{r_number}")
|
|
# TODO: Currently this can happen when multiple Blender instances are running and end up writing the same log entry to the .log file multiple times.
|
|
# This is not very ideal!
|
|
continue
|
|
previous_rev_number = r_number
|
|
|
|
r_msg_length = int(r_msg_length.split(" ")[0])
|
|
|
|
log_entry = repo.log.add()
|
|
log_entry.revision_number = r_number
|
|
log_entry.revision_author = r_author
|
|
|
|
log_entry.revision_date = r_date
|
|
log_entry.revision_date_simple = svn_date_simple(r_date).split(" ")[
|
|
0][5:]
|
|
|
|
# File change set is on line 3 until the commit message begins...
|
|
file_change_lines = chunk[2:-(r_msg_length+1)]
|
|
for line in file_change_lines:
|
|
line = line.strip()
|
|
status_char = line[0]
|
|
file_path = line[2:]
|
|
if ' (from ' in file_path:
|
|
# If the file was moved, let's just ignore that information for now.
|
|
# TODO: This can be improved later if neccessary.
|
|
file_path = file_path.split(" (from ")[0]
|
|
|
|
file_path = Path(file_path)
|
|
log_file_entry = log_entry.changed_files.add()
|
|
log_file_entry.svn_path = str(file_path.as_posix())
|
|
log_file_entry.revision = r_number
|
|
log_file_entry.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char]
|
|
|
|
log_entry['commit_message'] = "\n".join(chunk[-r_msg_length:])
|
|
log_entry.set_search_string()
|
|
|
|
|
|
def write_to_svn_log_file_and_storage(context, data_str: str) -> int:
|
|
"""
|
|
Get all SVN Log entries from the remote repo in the background,
|
|
without freezing up the UI, by calling this function every 3 seconds.
|
|
These are then stored in a file, so each log entry only needs to be fetched
|
|
once per computer that runs the addon.
|
|
|
|
Return how many SVN log entries were contained in data_str.
|
|
"""
|
|
repo = context.scene.svn.get_repo(context)
|
|
log_file_path = repo.log_file_path
|
|
|
|
file_existed = False
|
|
if log_file_path.exists():
|
|
file_existed = True
|
|
repo.reload_svn_log(context)
|
|
num_entries = len(repo.log)
|
|
|
|
with open(log_file_path, 'a+') as f:
|
|
# Append to the file, create it if necessary.
|
|
if file_existed:
|
|
# We want to skip the first line of the svn log when continuing,
|
|
# to avoid duplicate dashed lines, which would also mess up our
|
|
# parsing logic.
|
|
data_str = data_str[73:] # 72 dashes and a newline
|
|
data_str = "\n" + data_str # TODO: This is untested on windows.
|
|
|
|
# On Windows, the `svn log` command outputs lines with all sorts of \r and \n shennanigans.
|
|
# TODO: For this reason, this should be implemented with the --xml arg.
|
|
data_str = data_str.replace("\r", "")
|
|
if data_str.endswith("\n"):
|
|
data_str = data_str[:-1]
|
|
f.write(data_str)
|
|
|
|
repo.reload_svn_log(context)
|
|
|
|
print(f"SVN Log now at r{repo.log[-1].revision_number}")
|
|
return len(repo.log) - num_entries
|
|
|
|
|
|
class BGP_SVN_Log(BackgroundProcess):
|
|
name = "Log"
|
|
needs_authentication = True
|
|
timeout = 10
|
|
repeat_delay = 3
|
|
debug = False
|
|
|
|
def acquire_output(self, context, prefs):
|
|
"""This function should be executed from a separate thread to avoid freezing
|
|
Blender's UI during execute_svn_command().
|
|
"""
|
|
repo = context.scene.svn.get_repo(context)
|
|
|
|
latest_log_rev = 0
|
|
if len(repo.log) > 0:
|
|
latest_log_rev = repo.log[-1].revision_number
|
|
|
|
self.debug_print("Acquire output...")
|
|
|
|
# We have no way to know if latest_log_rev+1 will exist or not, but we
|
|
# must check, and there is no safe way to check it, so let's just
|
|
# catch and handle the potential error.
|
|
try:
|
|
self.output = execute_svn_command(
|
|
context,
|
|
["svn", "log", "--verbose",
|
|
f"-r{latest_log_rev+1}:HEAD", "--limit", "10"],
|
|
print_errors=False,
|
|
use_cred=True
|
|
)
|
|
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:
|
|
print("SVN: Log is now fully up to date.")
|
|
self.stop()
|
|
else:
|
|
self.error = error_msg
|
|
|
|
def process_output(self, context, prefs):
|
|
num_logs = write_to_svn_log_file_and_storage(context, self.output)
|
|
if num_logs < 10:
|
|
self.stop()
|
|
|
|
def get_ui_message(self, context):
|
|
repo = context.scene.svn.get_repo(context)
|
|
if len(repo.log) > 0:
|
|
rev_no = repo.log[-1].revision_number
|
|
return f"Updating log. Current: r{rev_no}..."
|
|
|
|
|
|
def svn_date_to_datetime(datetime_str: str) -> datetime:
|
|
"""Convert a string from SVN's datetime format to a datetime object."""
|
|
date, time, _timezone, _day, _n_day, _mo, _y = datetime_str.split(" ")
|
|
return datetime.strptime(f"{date} {time}", '%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
def svn_date_simple(datetime_str: str) -> str:
|
|
"""Convert a string form SVN's datetime format to a simpler format."""
|
|
dt = svn_date_to_datetime(datetime_str)
|
|
month_name = dt.strftime("%b")
|
|
date_str = f"{dt.year}-{month_name}-{dt.day}"
|
|
time_str = f"{str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
|
|
|
|
return date_str + " " + time_str
|