// This is a script for Directory Opus. // See http://www.gpsoft.com.au/DScripts/redirect.asp?page=scripts for development information. // Global vars (constants) var COLUMN_NAME_PREFIX = "SVN "; // With space at the end. var OPTION_NAME_PROPERTY_NOT_PRESENT_INDICATOR = "Property not present indicator"; var OPTION_NAME_PATH_TO_SVN_EXE = "Path to svn.exe"; var OPTION_NAME_LOG_SVN_WARNINGS = "Log svn warnings"; var OPTION_NAME_CUSTOM_SVN_PROPERTY_LIST = "Custom svn properties"; // Opus Events ------------------------------------------------------ function OnInit(initData) { //uid added via script wizard (do not change after publishing this script) var uid = "7476C9F6-7430-41CA-B013-CA601E2AF220"; //thread url added via script wizard (required for updating) var url = "http://resource.dopus.com/viewtopic.php?f=35&t=26492"; initData.name = "SVN Custom Property Columns"; initData.desc = "Provides custom SVN property columns."; initData.copyright = "by jsys in 2016 (RunHiddenEx & its dependency functions by tbone)"; initData.version = "1.1"; initData.default_enable = true; initData.min_version = "11.5.2"; // settings & defaults initData.config_desc = DOpus.Create.Map(); var option_name = undefined; option_name = OPTION_NAME_CUSTOM_SVN_PROPERTY_LIST; initData.Config[option_name] = initData.Config[option_name] = DOpus.NewVector("svn:externals", "tag"); initData.config_desc(option_name) = "Create your custom SVN property columns. One property (column) per line."; option_name = OPTION_NAME_LOG_SVN_WARNINGS; initData.Config[option_name] = initData.Config[option_name] = false; initData.config_desc(option_name) = "Capture and log warnings from svn.exe. Usually not useful except for troubleshooting this Add-In. Keep it off for measurably faster operation."; option_name = OPTION_NAME_PATH_TO_SVN_EXE; initData.Config[option_name] = "C:\\Program Files\\TortoiseSVN\\bin\\svn.exe"; initData.config_desc(option_name) = "Path to command line svn.exe, it optionally comes with TortoiseSVN, or you can download it from Apache website."; option_name = OPTION_NAME_PROPERTY_NOT_PRESENT_INDICATOR; initData.Config[option_name] = "-"; initData.config_desc(option_name) = "Customize indicator for when there's no target property."; // Final check if (DOpus.FSUtil.Exists(initData.Config[OPTION_NAME_PATH_TO_SVN_EXE]) == false) { DOpus.Output(initData.name + " Add-In error: svn.exe not found. You need to select \"command line client tools\" during TortoiseSVN installation.", true); } } function OnAddColumns(AddColData) { // Built-ins var svn_property_name = "proplist"; var col = AddColData.AddColumn(); col.name = COLUMN_NAME_PREFIX + svn_property_name; col.label = COLUMN_NAME_PREFIX + svn_property_name; col.method = "onListItem"; col.justify = "left"; col.multicol = false; // User-defined var enum_item = new Enumerator(Script.Config[OPTION_NAME_CUSTOM_SVN_PROPERTY_LIST]); for (; !enum_item.atEnd(); enum_item.moveNext()) { var svn_property_name = enum_item.item(); var col = AddColData.AddColumn(); col.name = COLUMN_NAME_PREFIX + svn_property_name; col.label = COLUMN_NAME_PREFIX + svn_property_name; col.method = "onListItem"; col.justify = "left"; col.multicol = false; // not supported //col.autorefresh = true; // overhead? } } function OnScriptConfigChange(ConfigChangeData) { Script.InitColumns(); } var row_count = 0; var is_current_path_valid = false; var is_flat_view = false; var current_column = ""; var column_cache = {}; var last_path = ""; var first_item_name = ""; function onListItem(scriptColData) { row_count += 1; if (row_count > scriptColData.Tab.stats.items || String(last_path) != String(scriptColData.Tab.path) || scriptColData.item + "".replace(/\\/gi, "/") == first_item_name) { first_item_name = scriptColData.item + "".replace(/\\/gi, "/"); row_count = 1; } if (row_count == 1) { last_path = scriptColData.Tab.path current_column = scriptColData.col; column_cache = {}; is_current_path_valid = true; if (scriptColData.Tab.stats.items == 0 || stringStartsWith(String(scriptColData.Tab.path), "ftp://") || isPathArchive(scriptColData.Tab.path)) { is_current_path_valid = false; } else { is_flat_view = false; var cmd = DOpus.Create.Command(); cmd.SetSourceTab(scriptColData.Tab); if (cmd.IsSet("FLATVIEW=toggle")) { is_flat_view = true; } } } if (is_current_path_valid == false) { scriptColData.value = ""; return; } var svn_property_name = scriptColData.col.slice(COLUMN_NAME_PREFIX.length); var operation = "propget"; if (svn_property_name == "proplist") // Special exception for built-in column. { svn_property_name = ""; operation = "proplist"; } if (scriptColData.Tab.stats.items == 1 || is_flat_view || isPathCollection(scriptColData.Tab.path)) // If there's only one item (file or folder) or Flat View is enabled or we're in a Collection (search/find etc.) use the slow serial retrieval mode. { handlePropertyColumnSlow(operation, svn_property_name, scriptColData); } else // Fast (cached) property-fetching-method. { handlePropertyColumnFast(operation, svn_property_name, scriptColData, row_count, column_cache); } } // Custom functions ------------------------------------------------- function stringStartsWith(str, prefix) { return str.indexOf(prefix) == 0; } function stringAfter(str, delimiter) { var start = str.indexOf(delimiter); if (start !== -1) { return str.slice(start + delimiter.length, str.length); } return str; } function stringUntil(str, delimiter) { var end = str.indexOf(delimiter); if (end !== -1) { return str.slice(0, end); } return str; } // ------------------------------------------------------------------ function isPathArchive(path) // This function is needed because Opus doesn't report directories inside archives as "file" but as "dir". { if (DOpus.FSutil.GetType(path, "a") == "file") { return true; } else { var sliced_path = String(path).split("\\"); var iterator = new Enumerator(sliced_path); var minimal_path = ""; for (; !iterator.atEnd(); iterator.moveNext()) { var path_part = iterator.item(); minimal_path += path_part; if (path_part.indexOf(".") !== -1 && DOpus.FSutil.GetType(minimal_path, "a") == "file") { return true; } minimal_path += "\\"; } return false; } } function isPathCollection(path) { return stringStartsWith(String(path), "coll://"); } // ------------------------------------------------------------------ function printSvnStdErr(str) { if (Script.Config[OPTION_NAME_LOG_SVN_WARNINGS] && str != "") { DOpus.Output(str); } } function filterSingleItemResult(operation, result) { if (result.stdout == "") { result.stdout = Script.Config[OPTION_NAME_PROPERTY_NOT_PRESENT_INDICATOR]; } else { if (operation == "propget") { result.stdout = stringUntil(result.stdout, "\r\n\r\n"); result.stdout = result.stdout.replace(/\r\n/g, "; "); result.stdout = result.stdout.replace(/\n/g, "; "); } else if (operation == "proplist") { result.stdout = stringAfter(result.stdout, "':\r\n ").replace("\r\n ", "; "); result.stdout = stringUntil(result.stdout, "\r\n"); } } return result; } function handlePropertyColumnSlow(operation, svn_property_name, scriptColData) { var result = RunHiddenEx(Script.Config[OPTION_NAME_PATH_TO_SVN_EXE], operation + " " + svn_property_name + " \"" + scriptColData.item + "\""); printSvnStdErr(result.stderr); result = filterSingleItemResult(operation, result); scriptColData.value = result.stdout + ""; } function handlePropertyColumnFast(operation, svn_property_name, scriptColData, row_count, column_cache) { if (row_count == 1) // Our script is invoked for the first time in current listing operation; tell the svn.exe to fetch the properties of all items (files & folders) in currently listed path at once, and then we will feed the Opus with cached data thus eliminating "write result, read result" cycles for *each* listed item -- so for the remaining items we work at the speed of the CPU rather than disk. Wow what a sausage of a comment! I could totally sneak some joke here. No ideas though. { preloadItemsProperties(column_cache, scriptColData.Tab.path, operation, svn_property_name); } if (row_count > scriptColData.Tab.stats.items) // just for debug purposes { if (scriptColData.Tab.stats.items > 0) // Ignore if zero items, this can happen if refreshing very rapidly before Opus manages to terminate the column instance. { DOpus.Output("row_count index overshoot: row_count: " + row_count + ", total items in tab: " + scriptColData.Tab.stats.items, true); // Report error, but this should never happen. } } else { scriptColData.value = getPreloadedSvnProperty(column_cache, scriptColData.item) + ""; } } function preloadItemsProperties(column_cache, path, operation, propget_property) { var result = RunHiddenEx(Script.Config[OPTION_NAME_PATH_TO_SVN_EXE], operation + " " + propget_property + " -R \"" + path + "\""); printSvnStdErr(result.stderr); var items_stdout = result.stdout.split("\r\n"); var last_item_path = ""; for (i = 0; i < items_stdout.length; i++) { var item_path_and_property = items_stdout[i].split(" - ", 2); if (operation == "propget") { if (item_path_and_property.length == 2) { item_path_and_property[0] = item_path_and_property[0].replace(/\\/gi, "/").toLowerCase(); // Regex to replace the backslashes with forward slashes, this way we can store the path safely in our array without those pesky backslashes acting as delimiters. column_cache[item_path_and_property[0]] = item_path_and_property[1].replace(/\n/gi, "; "); // Replace newlines (TortoiseSVN Properties GUI seems to write newlines as LF only so they get picked up all at-once here) with semicolons so that multilines can be displayed in our one-liner column. last_item_path = item_path_and_property[0] //DOpus.Output("[PRELOAD] " + item_path_and_property[0] + " -> " + item_path_and_property[1]); // debug text } else if (last_item_path != "" && item_path_and_property[0] != "") // Additional property value (after full CRLF newline), add them separated by semicolons. { column_cache[last_item_path] += "; " + item_path_and_property[0]; } } else if (operation == "proplist") { var PROPLIST_FILTER_OUT_STRING = "Properties on '"; if (stringStartsWith(String(item_path_and_property).toLowerCase(), PROPLIST_FILTER_OUT_STRING.toLowerCase())) { last_item_path = String(item_path_and_property).replace(/\\/gi, "/"); // Regex to replace the backslashes with forward slashes, this way we can store the path safely in our array without those pesky backslashes acting as delimiters. last_item_path = stringAfter(last_item_path, PROPLIST_FILTER_OUT_STRING); last_item_path = stringUntil(last_item_path, "':").toLowerCase(); // Clean up two extra trailing chars. try { column_cache[last_item_path] = ""; } catch(exception) { } } else if (last_item_path != "" && item_path_and_property[0] != "") { var delimiter = "; " if (column_cache[last_item_path] == "") // Prevent beginning with "; ". { delimiter = ""; } column_cache[last_item_path] += delimiter + stringAfter(String(item_path_and_property), " "); // Truncate two extra spaces at the beginning. } } } } function getPreloadedSvnProperty(column_cache, item) { var query_key = String(item).replace(/\\/gi, "/").toLowerCase() // Lowercase because svn.exe lowercases network shares when reporting paths (our key would be invalid). var ret_string = column_cache[query_key]; if (ret_string == undefined) { ret_string = Script.Config[OPTION_NAME_PROPERTY_NOT_PRESENT_INDICATOR]; } //DOpus.Output("[QUERY] " + query_key + " -> " + ret_string); // debug text return ret_string; } // tbone's functions ------------------------------------------------ function RunHiddenEx ( exe, params, tmpFileBase, tmpFileExt, shell, fso ){ //v1.2 //var milliseconds = (new Date).getTime(); if (!fso) fso = new ActiveXObject("Scripting.FilesystemObject"); if (!shell) shell = new ActiveXObject("WScript.Shell"); if (!tmpFileBase) tmpFileBase = "DO.Temp."; if (!tmpFileExt) tmpFileExt = ".txt"; var tmpFileStdout = GetTmpFileName(tmpFileBase, tmpFileExt, fso).fullname; var tmpFileStderr = tmpFileStdout+".err"+tmpFileExt; var cmd = '%comspec% /c ""'+exe+'" '+params+' >"'+tmpFileStdout+'" 2>"'+tmpFileStderr+'""'; if (Script.Config[OPTION_NAME_LOG_SVN_WARNINGS] == false) { cmd = '%comspec% /c ""'+exe+'" '+params+' >"'+tmpFileStdout+'""'; } var result = {}; result.cmd = cmd; result.returncode = shell.Run(cmd, 0, true); result.stdout = ReadFile(tmpFileStdout, fso); fso.DeleteFile(tmpFileStdout); if (Script.Config[OPTION_NAME_LOG_SVN_WARNINGS]) { result.stderr = ReadFile(tmpFileStderr, fso); fso.DeleteFile(tmpFileStderr); } else { result.stderr = ""; } //DOpus.Output("RunHiddenEx done in " + ((new Date).getTime() - milliseconds) + " ms"); return result; } /////////////////////////////////////////////////////////////////////////////// function ReadFile ( path, fso ){ fso = fso || new ActiveXObject("Scripting.FilesystemObject"); var content = ""; if (!fso.FileExists(path)){ return content; } var file = fso.OpenTextFile( path, 1, -2); // Read, UseDefaultEncoding if (!file.AtEndOfStream) content = file.ReadAll(); file.Close(); return content; } /////////////////////////////////////////////////////////////////////////// function GetTmpFileName(prefix, extension, fso) { fso = fso || new ActiveXObject("Scripting.FilesystemObject"); var tFolder = fso.GetSpecialFolder(2); //2 = temp folder var tFile = fso.GetTempName(); if (prefix!=undefined) tFile=prefix+tFile; if (extension!=undefined) tFile+=extension; return { path : tFolder.Path, name : tFile, fullname: tFolder.Path+'\\'+tFile }; } // ------------------------------------------------ /////////////////////////////////////////////////////////////////////////////// function OnAboutScript(data){ //v0.1 var cmd = DOpus.Create.Command(); if (!cmd.Commandlist('s').exists("ScriptWizard")){ if (DOpus.Dlg.Request("The 'ScriptWizard' add-in has not been found.\n\n"+ "Install 'ScriptWizard' from [resource.dopus.com].\nThe add-in enables this dialog and also offers "+ "easy updating of scripts and many more.","Yes, take me there!|Cancel", "No About.. ", data.window)) cmd.RunCommand('http://resource.dopus.com/viewtopic.php?f=35&t=23179');} else cmd.RunCommand('ScriptWizard ABOUT WIN='+data.window+' FILE="'+Script.File+'"'); } //MD5 = "6372a6ec2c2106f6588cdf757cc528a1"; DATE = "2016.11.29 - 18:33:09"