# 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