-- -- File:: pipeline/util/p4.ms -- Description:: Perforce bindings for MaxScript. -- -- Author:: David Muir -- Date:: 21 June 2010 -- -- NOTE: please put higher-levels functions into 'p4_utils.ms'; the idea was -- that this was a thin wrapper around the P4API. -- -- This is set at end of script: global gRsPerforce -- Rollout used to request passwords for Perforce: try (destroyDialog RsP4PasswordDialog) catch () rollout RsP4PasswordDialog "Perforce Connect" width:300 ( local standAlone = true local ctrlPositions local textWidth = 280 label messageLabel "" align:#left width:textWidth height:300 listbox serverList "Perforce Server Setups:" align:#left height:1 selection:0 width:273 pos:(messageLabel.pos + [-6,10]) label pwLabel "Password:" align:#left pos:(serverList.pos + [0,12]) dotNetControl txtPassword "System.Windows.Forms.TextBox" width:150 pos:(pwLabel.pos + [(GetTextExtent pwLabel.text).x + 5,-3]) button btnLogin "Login" width:100 align:#left offset:[0,15] across:2 button btnCancel "Cancel" width:100 align:#right offset:[0,15] fn setServerList = ( if (::gRsPerforce.p4 == undefined) do ( gRsPerforce.connect() ) local serverInfoList = gRsPerforce.getP4infoList regen:true -- Get the current Perforce-settings: local currentServer = gRsPerforce.p4.Port local currentWorkSpace = gRsPerforce.p4.Client local currentUsername = gRsPerforce.p4.User local currentServerNum = 0 local serverListItems = #() serverListItems = for serverNum = 1 to serverInfoList.count collect ( local serverInfo = serverInfoList[serverNum] -- Is this the currently-used server? local isCurrentServer = false if (currentServerNum == 0) do ( if (serverInfo.server == currentServer) and (serverInfo.workspace == currentWorkSpace) and (serverInfo.username == currentUsername) do ( currentServerNum = serverNum isCurrentServer = true ) ) (if isCurrentServer then "* " else " ") + serverInfo.name + " - " + serverInfo.server + " - " + serverInfo.workspace ) serverList.items = serverListItems serverList.selection = currentServerNum ) fn setupLoginCtrls = ( messageLabel.text = RsWordWrap (trimRight gRsPerforce.loginMsg) textWidth -- Keep a copy of the original control-positions, to revert to if function is re-run: if (ctrlPositions == undefined) then ( ctrlPositions = for ctrl in RsP4PasswordDialog.controls collect ctrl.pos ) else ( -- Restore control positions if re-running: for n = 1 to ctrlPositions.count do ( RsP4PasswordDialog.controls[n].pos = ctrlPositions[n] ) if ::gRsPerforce.loginMsg == "" then ( messageLabel.text = "Login successful!" ) else ( -- Clear password text for retry: txtPassword.text = "" ) ) -- Clear error-message: gRsPerforce.loginMsg = "" local textHeight = (RsGetTextExtent messageLabel.text).y -- Shift server-list down to fit text: serverList.pos.y += textHeight setServerList() -- Resize the list to fit its contents: serverList.height = (serverList.items.count * 13) +6 -- Move other controls down below list: local moveCtrls = #(pwLabel, txtPassword, btnLogin, btnCancel) for ctrl in moveCtrls do ( ctrl.pos.y += (serverList.height + textHeight) ) -- Resize rollout: RsP4PasswordDialog.height = btnCancel.pos.y + 30 ) fn loginPressed = ( if (serverList.selection != 0) do ( ::gRsPerforce.login serverList.selection txtPassword.text createNewDialogOnFail:standAlone destroyDialog RsP4PasswordDialog if RsP4PasswordDialog.open do ( setupLoginCtrls() ) ) ) fn cancelPressed = ( gRsPerforce.lastLoginSucceded = false if standAlone then ( destroyDialog RsP4PasswordDialog ) else ( removeRollout RsP4PasswordDialog RsPerforceToolFloater addRollout RsP4PasswordDialog RsPerforceToolFloater ) ) on txtPassword KeyPress ev arg do ( case of ( (arg.keyChar == "\r"):(loginPressed()) -- Enter pressed ((bit.charAsInt arg.keyChar) == 27):(cancelPressed()) -- Escape pressed ) ) on btnLogin pressed do ( loginPressed() ) on btnCancel pressed do ( cancelPressed() ) on RsP4PasswordDialog open do ( -- Is rollout docked into a floater or not? standAlone = ((getDialogSize RsP4PasswordDialog) != [0,0]) -- If rollout is docked, hide the "Cancel" button, and move "Login" over to middle: if not standAlone do ( btnCancel.visible = false btnLogin.pos = btnLogin.pos + ((btnCancel.pos - btnLogin.pos) / 2) ) setupLoginCtrls() -- Set up dotNet password-box: txtPassword.Height = 20 txtPassword.multiLine = true txtPassword.acceptsTab = true txtPassword.PasswordChar = "*" txtPassword.text= ::gRsPerforce.P4password as string setFocus txtPassword -- Set password-box colors to match UI: local textCol = (colorMan.getColor #windowText) * 255 local windowCol = (colorMan.getColor #window) * 255 local DNcolour = dotNetClass "System.Drawing.Color" txtPassword.foreColor = DNcolour.FromArgb textCol[1] textCol[2] textCol[3] txtPassword.backColor = DNcolour.FromArgb windowCol[1] windowCol[2] windowCol[3] ) ) -- -- struct: Perforce -- desc: Perforce functions for MaxScript. -- -- notes: -- Just like any Perforce API please batch your requests as much as possible; -- by passing MaxScript Array objects as file arguments. -- -- examples: -- p4 = RsPerforce() -- (RsPerforce false P4:dotNetObject:P4API.P4Connection result:undefined) -- p4.connect "x:\\payne" -- Successfully connect to the Perforce server (rsgvanp4s1:1666 as david.muir). -- true -- p4.local2depot "x:\\payne\\build\\dev\\levelstatus.txt" -- "//projects/payne/build/dev/levelstatus.txt" -- p4.FileMapTo #local "//depot/gta5/assets_ng/export/levels/gta5/_cityw/beverly_01/bh1_01.zip" -- "X:\gta5\assets_ng\export\levels\gta5\_cityw\beverly_01\bh1_01.zip" struct RsPerforce ( p4 = undefined, -- .Net P4API.P4Connection object. result = undefined, -- Last P4.Net result (RecordSet) or undefined. currScmGroup = 0, fileMapping = undefined, p4rootPattern = "*", -- Used by RsP4PasswordDialog: P4password = "", loginMsg = "", lastLoginSucceded = false, -- Used by PostExportAdd: addQueue = #(), -- Used by SubmitFilesWithText: newListText, -- The server-list is only generated when it needs to be, to avoid garbage-collect errors from RsProjectGetPerforceInfo: p4infoList, fn getP4infoList regen:false = ( if regen or (p4infoList == undefined) do ( p4infoList = for n = 1 to RsProjectGetNumPerforces() collect (RsProjectGetPerforceInfo (n - 1)) ) return p4infoList ), -- Calls up a login-rollout, and returns success true/false: fn getPerforcePassword = ( destroyDialog RsP4PasswordDialog if RsProjectGetPerforceIntegration() then ( createDialog RsP4PasswordDialog style:#(#style_titlebar, #style_border) width:300 modal:true return ::gRsPerforce.lastLoginSucceded ) else ( return false ) ), -- -- name: connect -- desc: Connect to the Perforce server that has dir mapped. -- -- This required your Perforce environment to be set up correctly. -- fn connect = ( if not RsProjectGetPerforceIntegration() do ( return false ) local outVal = false local currentDir = sysInfo.currentdir try ( sysInfo.currentdir = (RsConfigGetProjRootDir()) if ( p4 == undefined ) then p4 = dotNetObject "P4API.P4Connection" if ( fileMapping == undefined) then fileMapping = dotNetClass "RSG.SourceControl.Perforce.FileMapping" p4.cwd = sysInfo.currentdir p4.connect() p4.run "login" #("-s") format "Successfully connect to the Perforce server (% as %).\n" p4.Port p4.User outVal = (p4.isValidConnection true true) ) catch ( local message = "Exception connecting to Perforce:\n" + (getCurrentException()) format "%\n" message loginMsg = message getPerforcePassword() sysInfo.currentdir = currentDir outVal = lastLoginSucceded ) -- If connection was successful, extract client-root from Perforce-info: p4rootPattern = "*" if outVal do ( -- Get P4 root-path ("x:/*") to allow 'fn Run' to tell if a path is under the P4 root or not: local P4info = This.Info() p4rootPattern = (P4info.ClientRoot + "*") ) sysInfo.currentdir = currentDir return outVal ), fn dump = ( format "User %\nPort %\nHost %\nClient %\n" p4.User p4.Port p4.Host p4.client ), -- -- name: connected -- desc: Return true if successfully connected; false otherwise. -- fn connected = ( pushPrompt "Checking P4 connection..." local retVal = (p4 != undefined) and (p4.IsValidConnection true true) popPrompt() return retVal ), fn logout = ( try( p4.run "logout" #() )catch() return true ), -- -- name: disconnect -- desc: Disconnect from the Perforce server. -- fn disconnect = ( try( logout() p4.Disconnect() )catch ( print (getCurrentException()) return false ) return true ), -- -- name: run -- desc: Generic run method that maps straight onto the low-level P4API.P4 object. -- If "showProgress" is used, non-file arguments must be supplied via "switches", -- as the "args" list will be split up into chunks -- fn run cmd args switches:#() showProgress:false progressTitle: returnResult:false silent:false checkEmptyArgs:true doLogin:true = ( -- Skip command if Perforce integration is turned off: if (not RsProjectGetPerforceIntegration()) do ( return (if returnResult then undefined else True) ) -- Log this Perforce call; this is for Perforce command tracking. -- Its too easy to kill Perforce if you're not careful. if ( undefined != gRsULog ) then ( local logMessage = stringStream "" format "RsPerforce::Run( % % )" cmd args to:logMessage gRsULog.LogDebug (logMessage as String) context:"p4.ms" ) local failReturn = if returnResult then undefined else False if (args == undefined) or (args == "") then ( format "Empty string as perforce parameter!\n" return failReturn ) if not isKindOf args Array do ( args = #(args) ) if ( checkEmptyArgs and args.count == 0 ) do ( format "Empty array passed as perforce args! (switches: %)\n" (switches as string) gRsULog.LogWarning ("P4 command with 0 arguments passed, for command " + cmd + " - should this have arguments?" ) context:"P4.ms" return failReturn ) if not connected() do ( if (not doLogin) or (not (connect())) do ( if doLogin do ( format "Perforce login failed.\n" ) return failReturn ) ) -- Make paths p4-legal: args = for thisArg in args collect ( -- We don't want to "fix" the double-slashes at the start of //depot/ path: local doubleSlashStart = (matchPattern thisArg pattern:"//*") -- Remove double-slashes, convert to forward-slashes: thisArg = RsMakeSafeSlashes thisArg -- Add double-slash back for depot-paths: if doubleSlashStart do ( thisArg = ("/" + thisArg) ) -- Don't process arg if it's a legal path with drive-letter specified, that isn't under P4 root: -- Non-client files will cause errors -- (this should ignore non-path args) if (thisArg[2] == ":") and (thisArg[1] != "-") and (pathConfig.isLegalPath thisArg) and (not matchPattern thisArg pattern:p4rootPattern) do ( thisArg = DontCollect ) thisArg ) -- Abort if all files were filtered out: if ( checkEmptyArgs and args.count == 0 ) do ( return failReturn ) if showProgress and (progressTitle == unsupplied) do ( progressTitle = "P4.run " + cmd + " (" + (args.count as string) + " files)" ) -- If this function is updating a progress-bar, chop the file-list up into chunks: local fileArgList = #() if showProgress then ( progressStart progressTitle local maxChunkSize = args.count / 15.0 local currentChunk local nextChunk = 0 for n = 1 to args.count do ( if (n > nextChunk) or (n == args.count) do ( nextChunk += maxChunkSize currentChunk = #() join currentChunk switches append fileArgList currentChunk ) append currentChunk args[n] ) ) else ( append fileArgList #() join fileArgList[1] switches join fileArgList[1] args ) local errors = #() local warnings = #() local messages = #() local errorMessage = "" local notCancelled = true local useArgs local useCommand = "" -- Used by error-catch local results = #() local numRetries = 3 local success = false for retry=1 to numRetries while (not success) do ( try ( for n = 1 to fileArgList.count while (not showProgress) or (notCancelled = progressUpdate (100.0 * n / fileArgList.count)) do ( useArgs = fileArgList[n] useCommand = "gRsPerforce.p4.run \"" + cmd + "\" " + (useArgs as string) if not silent do (format "%\n" useCommand) -- Process the RecordSet into something to return to MaxScript: result = (p4.Run cmd useArgs) -- Build results-list: append results result join errors result.Errors join messages result.messages if result.errorMessage != "" do (errorMessage = result.errorMessage) -- Don't show warnings for up-to-date files: join warnings (for warning in result.Warnings where (not matchpattern warning pattern:"* - file(s) up-to-date.") collect warning) ) success = true --Do not retry. ) catch ( local exception = getCurrentException() --if not silent do ( format "P4 Error: %\n%\n" useCommand exception ) local connectionReset = ((findstring exception "WSAECONNRESET") != undefined) or ((findstring exception "Partner exited unexpectedly") != undefined) local rsULogMethod = undefined if connectionReset == false then ( rsULogMethod = gRsULog.LogWarning notCancelled = false ) else ( if retry == numRetries then ( --Users are unable to connect to the Perforce server. Prompt users that the command has failed. errorMessage = ("Perforce has become unresponsive. Waiting before retrying...\n\n" + cmd + " " + useArgs as string) if false == (RsQueryBoxTimeOut errorMessage title:"Perforce Command Error" yesText:"Try Again" noText:"Cancel" ) then ( --Cancel, log this as an error to stop the export. notCancelled = false rsULogMethod = gRsULog.LogError exception = "Command has been aborted by the user." ) else ( --Continue to try. retry = 0 ) ) ) if rsULogMethod != undefined then ( local tokens = filterstring (exception) "\n" if tokens != undefined do ( for n = 1 to tokens.count do ( local token = tokens[n] token = substituteString token "\\" "/" token = substituteString token "" "_" rsULogMethod token context:"P4.ms" ) ) ) ) ) if showProgress do ( progressEnd() -- Blank out the progress-text, as proper redraw might not happen for a while: progressStart ""; progressUpdate 100.0; progressEnd() ) -- Return array of results if requested: if returnResult do ( if notCancelled then ( -- showProgress generates multiple result-records, so returns an array of them: if showProgress then ( return results ) else ( return result ) ) else ( return undefined ) ) if (not notCancelled) or (errors.count != 0) or (warnings.count != 0) then ( -- Display errors in a messagebox and output to listener, if not running silently: if not silent do ( local dontError = (RsUserGetFriendlyName()=="Tools" and cmd=="submit") for er in errors do ( if dontError then gRsUlog.LogWarning (er as string) else gRsUlog.LogError (er as string) ) for m in messages do gRsUlog.LogMessage (m as string) for w in warnings do gRsUlog.LogWarning (w as string) ) return false ) return true ), -- -- name: login -- desc: login with given group id -- fn login id pw createNewDialogOnFail:false = ( currScmGroup = id P4password = pw getP4infoList() local P4Info = p4infoList[id] format "P4 login: %\n" (P4Info as string) case of ( (id > p4infoList.count): ( messagebox ("Id higher than exisiting server setups: "+ p4infoList.count as string) return false ) (P4Info == undefined): ( messagebox ("Invalid Perforce-setup item: " + (id - 1) as string) return false ) ) try ( disconnect() p4.User = P4Info.username p4.Port = P4Info.server p4.Client = P4Info.workspace p4.connect() p4.login pw result = p4.run "login" #("-s") if (result != undefined) do ( if (result.HasErrors()) or (result.HasWarnings()) then ( local msg = stringstream "Login result:\n" format "%: %\n" "ErrorMessage" (result.ErrorMessage as string) to:msg format "%: %\n" "Errors" (result.Errors as string) to:msg format "%: %\n" "Item" (result.item as string) to:msg format "%: %\n" "Messages" (result.Messages as string) to:msg format "%: %\n" "Records" (result.Records as string) to:msg format "%: %\n" "Warnings" (result.Warnings as string) to:msg -- messagebox msg throw msg ) ) lastLoginSucceded = true ) catch ( local message = getCurrentException() if (message == undefined) do ( message="Perforce command returned undefined result." ) disconnect() loginMsg = message getPerforcePassword() ) return returnVal ), -- Converts a Perforce result-record to a Max struct: fn record2struct record = ( local structString = stringStream "" format "struct p4recordStruct (" to:structString local keyNames = record.fields.keys local keyCount = record.fields.keys.count for n = 1 to keyCount do ( local keyname = keyNames[n] local key = RsFileSafeString keyname local item = RsMakeSafeSlashes (record.fields.item keyname) format "% = \"%\"" key item to:structString if (n != keyCount) do ( format ", " to:structString ) ) format ")" to:structString -- Return struct instance, converted from stringstream: (execute (structString as string))() ), -- Returns struct containing Perforce info: fn info = ( result = run "info" #() returnResult:true silent:true checkEmptyArgs:false if result == undefined then undefined else ( record2struct result.records[1] ) ), -- Finds/fixes files that have been deleted locally, but not on the server: -- (not as thorough as a full Reconcile, but useful as a quick fix for an issue we see regularly...) fn deleteSyncCheck filenames doFix:True silent:True = ( -- Filter out filenames that exist locally, we don't need to see if they've been deleted from server: local filenamesList = filenames if (not isKindOf filenames Array) do ( filenamesList = #(filenames) ) filenamesList = for filename in filenamesList where (not doesFileExist filename) collect filename if (filenamesList.count ==0) do (return #()) -- Get list of files that have been locally deleted, but are still on Perforce: pushPrompt "Checking for locally-deleted files..." local result = run "reconcile" filenamesList switches:#("-d", "-n") returnResult:True silent:silent popPrompt() if (result == undefined) do (return undefined) local records = result.Records local delFilenames = for i = 1 to (records.Count) collect ( records[i].Item["depotFile"] ) if (delFilenames.count != 0) do ( -- Set have-version of wonky files to #0 - they'll be synced by next proper sync: -- ("flush" sets the synced-flag without attempting file-copy actions, although that may not be necessary here) if (doFix) and (delFilenames.count != 0) do ( format "Fixing locally-deleted files: Setting have-version to #0\n" for thisFile in delFilenames do ( format " %\n" thisFile ) pushPrompt "Fixing have-version for locally-deleted files" local fixFilenames = for thisFile in delFilenames collect (thisFile + "#0") run "flush" fixFilenames silent:silent popPrompt() ) ) return delFilenames ), -- Returns list of checked-out filenames: fn checkedOut = ( local outVal = #() local infoResult = run "info" #() returnResult:true silent:true checkEmptyArgs:false if (infoResult != undefined) do ( local clientName = infoResult.records[1].fields.item["clientName"] local prefixRemove = clientName.count + 4 local clientRoot = infoResult.records[1].fields.item["clientRoot"] local openedResult = run "opened" #() returnResult:true silent:true checkEmptyArgs:false outVal = for item in openedResult.records collect ( toLower (clientRoot + (substring item.fields.item["clientFile"] prefixRemove -1)) ) ) outVal ), -- Get set of file-status records: fn getFileStats files silent:true = ( local fileStats = run "fstat" files silent:silent returnResult:true if (fileStats == undefined) do return undefined for record in result.records collect record ), -- This checks the file list to see if they are checked out for edit/add by the current user -- already. It sets the corresponding element to true in the bitarray if it is. fn checkedOutForEdit files silent:true = ( local retVal = #{} if (files.count == 0) do return retVal local results = files if (not isKindOf files[1] dotNetObject) do ( results = getFileStats files silent:silent ) if (results == undefined) do return retVal retVal.count = results.count for fileNum = 1 to results.count do ( local action = results[fileNum].Item["action"] if (action == "edit") or (action == "add") do ( retVal[fileNum] = true ) ) return retVal ), -------------------------------------------------------------- -- Returns bitarray of files that are exclusively -- checked out by someone else -------------------------------------------------------------- fn isCheckedoutExclusive files silent:true = ( local retVal = #{} if (files.count == 0) do return retVal local results = files if (not isKindOf files[1] dotNetObject) do ( results = getFileStats files silent:silent ) if (results == undefined) do return retVal retVal.count = results.count for fileNum = 1 to results.count do ( local record = results[fileNum] -- otherOpen key is undefined if file isn't checked out by anyone else: local otherOpened = (record.fields.item["otherOpen"] != undefined) if otherOpened do ( -- Does filetype have +l flag set? local fileType = record.fields.item["headType"] local typeFlags = (filterString fileType "+")[2] if (typeFlags != undefined) and (matchPattern typeFlags pattern:"*l*") do ( retVal[fileNum] = true ) ) ) return retVal ), -- Sync to latest revision of the specified file(s) on Perforce: fn sync filename force:false silent:false showProgress:false progressTitle: = ( run "sync" filename switches:(if force then #("-f") else #()) silent:silent showProgress:showProgress progressTitle:progressTitle ), -- Syncs files, after fixing records for files that have been deleted locally: -- (this used to perform similar function by force-getting missing files after initial sync - hence the name) fn syncWithRetry filenames force:false silent:false showProgress:false progressTitle: = ( if not isKindOf filenames Array do ( filenames = #(filenames) ) -- No need to perform this check when force-syncing: if (not force) do ( -- Make sure that none of these files are locally deleted outside the server's knowledge: local result = deleteSyncCheck filenames doFix:True silent:silent if (result == undefined) do (return undefined) ) sync filenames force:force silent:silent showProgress:showProgress progressTitle:progressTitle ), -- Returns filenames that have changed on the server, or undefined if check failed: fn changedFiles filenames silent:true showProgress:false progressTitle: = ( -- Make sure that none of these files are locally deleted, outside the server's knowledge: local result = deleteSyncCheck filenames doFix:True silent:silent if (result == undefined) do (return undefined) if not silent do (format "Getting P4 changes: ") local results = run "sync" filenames switches:#("-n") silent:silent returnResult:true showProgress:showProgress progressTitle:progressTitle if (results == undefined) then (return undefined) else ( -- Results is returned as an array when showProgress is used: if not (isKindOf results array) do (results = #(results)) local outList = #() for result in results do ( for record in result.records do ( append outList record.item["clientFile"] ) ) return outList ) ), -- Sync only changed files, allowing for better progress-bar showing: -- Can set limit for minimum number of sync-items before progress-bar is shown fn syncChanged filenames force:false clobber:false clobberAsk:true silent:true minForProgress:0 showProgress:true progressTitle: = ( if RsProjectGetPerforceIntegration() do ( local getFilenames if force then ( getFilenames = filenames ) else ( local changedFilenames = changedFiles filenames silent:silent if (changedFilenames == undefined) do (return false) -- Find writable files: local writableFiles = #{} for fileNum = 1 to changedFilenames.count do ( local filename = changedFilenames[fileNum] writableFiles[fileNum] = (doesFileExist filename) and not (getfileattribute filename #readOnly) ) local clobberFilenames = for n = writableFiles collect changedFilenames[n] local getFileNums = #{} getFileNums.count = changedFilenames.count getFileNums = -getFileNums if (writableFiles.numberSet != 0) and clobberAsk and not clobber do ( clobberAsk = false local plural = if (clobberFilenames.count == 1) then "" else "s" local pronoun = if (clobberFilenames.count == 1) then "it" else "them" local queryVal = RsQueryBoxMultiBtn ("Writable file" + plural + " found.\nClobber (overwrite) " + pronoun + "?") title:("Sync over writable file" + plural + "?") \ labels:#("Clobber File" + plural, "Ignore File" + plural) tooltips:#("Overwrite these files", "Don't overwrite these files") \ width:600 timeout:30 defaultBtn:1 listItems:(for item in clobberFilenames collect (RsMakeBackSlashes item)) clobber = (queryVal == 1) ) if clobber then ( for filename in clobberFilenames do ( setfileattribute filename #readOnly true ) ) else ( getFileNums -= writableFiles ) -- Don't sync readonly files, but make them writable if clobber is turned on local getFilenames = for filename in changedFilenames collect ( local getItem = filename if (doesFileExist filename) and not (getfileattribute filename #readOnly) do ( if clobberAsk and not clobber do ( clobberAsk = false clobber = queryBox "Writable file(s) found. Clobber (overwrite) them?" title:"Sync over writable file(s)?" ) if clobber then ( setfileattribute filename #readOnly true ) else ( getItem = dontCollect ) ) getItem ) if (getFilenames == undefined) do (return false) if (getFilenames.count == 0) do (return true) if not silent do (format "(% files have changed on server)\n" getFilenames.count) ) if showProgress and (getFilenames.count < minForProgress) do ( showProgress = false ) sync getFilenames force:force silent:silent showProgress:showProgress progressTitle:progressTitle ) ), -- Checkout current revision of the specified file(s) from Perforce: fn edit filename exclusive:false silent:false switches:#() = ( local filenames = if isKindOf filename Array then filename else #(filename) -- If Perforce integration is turned off, just make the file writable: if not RsProjectGetPerforceIntegration() do ( for filename in filenames where doesFileExist filename do ( setFileAttribute filename #readOnly false ) return true ) if not (run "edit" filenames silent:silent switches:switches) do ( return false ) if exclusive do ( if not (run "reopen" filenames switches:#("-t", "+l") silent:silent) do ( return false ) ) return true ), -- Mark the file for add on Perforce (default changelist) fn add filename exclusive:false silent:false switches:#() = ( -- If Perforce integration is turned off, just make these files writable. -- Loops through just incase an array of multiple filenames given if not RsProjectGetPerforceIntegration() do ( if ( classof filename == Array ) then ( for file in filename where doesFileExist file do ( setFileAttribute file #readOnly false ) ) else ( setFileAttribute filename #readOnly false ) return true ) if not (run "add" filename silent:silent switches:switches) then return false if exclusive do ( if not (run "reopen" filename switches:#("-t", "+l") silent:silent) then return false ) ), fn reopen filenames filetype: exclusive:true silent:false = ( if not isKindOf filenames Array do ( filenames = #(filenames) ) local fileTypeString = "" if unsupplied!=filetype then append fileTypeString filetype if exclusive then ( -- need to check for existing "+" as causing problems like "binary+m+l" local tok = FilterString fileTypeString "+" if tok.count == 0 or tok.count == 1 then append fileTypeString "+l" -- +l = exclusive open else( fileTypeString = tok[1] -- e.g. "binary" append fileTypeString "+" -- "binary+" if not (matchPattern tok[2] pattern:"*l*") then ( append fileTypeString "l" -- "binary+l" ) append fileTypeString tok[2] -- "binary+lmS16....." ) ) run "reopen" filenames switches:#("-t", fileTypeString) silent:silent ), -- -- fn exists -- desc Checks a list of files to see if they are in perforce already, and -- sets their index to false in the filesInP4List if it doesn't -- fn exists &fileList silent:true = ( local result = undefined local filesInP4List = #{} filesInP4List.count = fileList.count if ( fileList.count > 0 ) do ( result = ( run "fstat" fileList silent:silent returnResult:true ) -- Array of files passed if ( (result != undefined) and (result.records.count > 0) ) then ( -- If the clientFile matches the one in the record then it's in perforce so no -- need to add it/create it later unless forced local unMatchedRecords = (for n = 1 to result.records.count collect n) as bitarray local recordNames = for record in result.records collect (toLower (RsMakeSafeSlashes record.fields.item["clientFile"])) local recordHeadActions = for record in result.records collect (toLower (RsMakeSafeSlashes record.fields.item["headAction"])) for fileNum = 1 to fileList.count do ( local findFilename = toLower (RsMakeSafeSlashes fileList[fileNum]) local keepLooking = true for recordNum in unMatchedRecords while keepLooking do ( if (recordNames[recordNum] == findFilename and recordHeadActions[recordNum] != "delete") do ( filesInP4List[fileNum] = true unMatchedRecords[recordNum] = false keepLooking = false ) ) ) ) ) filesInP4List ), -- -- fn files -- desc Returns a list of all the files in a given directory that have not been deleted -- fn files directory silent:true = ( local p4FileList = #() result = (run "files" directory silent:silent returnResult:true) if ( (result != undefined) and (result.records.count > 0)) then ( for i = 1 to result.records.count do ( local record = result.Records[i] if (record != undefined) then ( local depotFile = record.Fields.Item["depotFile"] local action = record.Fields.Item["action"] if (action != "delete") then append p4FileList depotFile ) ) ) p4FileList ), fn IsQueueEmpty = ( addQueue.count==0 ), -- Used to add files to Perforce after export, queued up beforehand: -- RsPerforce.addQueue is used if queue: argument is unsupplied fn postExportAdd exclusive:false silent:false cancel:false changelistNum: queue: = ( if (queue == unsupplied) do ( queue = addQueue ) if cancel or not RsProjectGetPerforceIntegration() do ( return true ) local retVal = true local switches = #() if (changelistNum != unsupplied and changelistNum != undefined) then ( switches = #("-c", changelistNum as string) ) -- Only add files that exist locally: local addFiles = for filename in queue where (doesFileExist filename) collect filename if (addFiles.count != 0) do ( retVal = add addFiles exclusive:exclusive silent:silent switches:switches -- Remove files we marked for add in perforce, as there could be some which are in the queue -- which haven't been created yet and might get added later. for fileIdx = queue.count to 1 by -1 do ( if (findItem addFiles queue[fileIdx] != 0) do ( deleteItem queue fileIdx ) ) ) retVal ), -- Add or edit wrapper -- If queueAdd is true, add-files will be added to queue array, to be processed later by PostExportAdd -- if queue is unsupplied, RsPerforce.addQueue array is used instead. fn add_or_edit filenames exclusive:false silent:false queueAdd:false queue: switches:#() = ( local retVal = true if not isKindOf filenames Array do ( filenames = #(filenames) ) -- If Perforce integration is turned off, just make these files writable: if not RsProjectGetPerforceIntegration() do ( for filename in filenames where (doesFileExist filename) do ( setFileAttribute filename #readOnly false ) return true ) -- Set queueAdd to true if queue is specified: if (queue == unsupplied) then ( queue = addQueue ) else ( queueAdd = true ) if (filenames.count != 0) do ( -- Check to see which filenames are already in Perforce: local existingP4FileNums = exists filenames local onServerFiles = for n = existingP4FileNums collect filenames[n] local notOnServerFiles = for n = -existingP4FileNums collect filenames[n] local notOnClientFiles = #() local noPermissionFiles = #() local notCheckedOutFiles = #() if ( onServerFiles.count > 0 ) do ( pushPrompt ("Perforce: Edit files: " + existingFiles as string) retVal = run "edit" onServerFiles switches:switches silent:silent returnResult:true popPrompt() if (retVal == undefined) then (return false) -- Get list of missing files: local notOnClientWarn = "* - file(s) not on client." local noSuchFileWarn = "* - no such file(s)." local noPermissionWarn = "* - no permission for operation on file(s)." for warn in retVal.warnings do ( case of ( (matchPattern warn pattern:notOnClientWarn): ( append notOnClientFiles (substring warn 1 (warn.count - (notOnClientWarn.count - 1))) ) (matchPattern warn pattern:noPermissionWarn): ( append noPermissionFiles (substring warn 1 (warn.count - (noPermissionWarn.count - 1))) ) (matchPattern warn pattern:noSuchFileWarn): ( append notOnServerFiles (substring warn 1 (warn.count - (noSuchFileWarn.count - 1))) ) ) ) ) -- Edit non-synced files: if (notOnClientFiles.count != 0) do ( -- Switch -k sets file-status to fully synced, even if it's not: pushPrompt ("Perforce: Not on client, attempting to set as Synced: " + notOnClientFiles as string) run "sync" notOnClientFiles switches:#("-k") silent:silent popPrompt () -- Brings up warning-window if edit still fails: pushPrompt ("Perforce: Retrying file Edit: " + notOnClientFiles as string) retVal = run "edit" notOnClientFiles switches:switches silent:silent returnResult:true popPrompt() if (retVal != undefined) do ( -- Anything that's still marked as Not On Client will need to be added to the Add list: for warn in retVal.warnings where (matchPattern warn pattern:notOnClientWarn) do ( append notOnServerFiles (substring warn 1 (warn.count - (notOnClientWarn.count - 1))) ) ) ) retVal = true -- Add missing files, or add them to queue to be processed later by PostExportAdd: if (notOnServerFiles.count != 0) do ( join NotCheckedOutFiles NotOnServerFiles if queueAdd then ( if not silent do ( format "Queueing files for add: %\n" (notOnServerFiles as string) ) join queue notOnServerFiles ) else ( retVal = run "add" notOnServerFiles switches:switches silent:silent ) ) -- Warn if user doesn't have checkout permission for some files, and ask to make them writable instead: if (NoPermissionFiles.count != 0) do ( join NotCheckedOutFiles NoPermissionFiles format "ERROR: No permission to checkout:\n" for ThisFile in NoPermissionFiles do ( format " %\n" ThisFile gRsUlog.LogWarning ("No permission to checkout file: " + ThisFile) ) ) ) if not retVal do (return false) -- Set files as exclusive-checkouts: if exclusive do ( local existingFiles = for filename in filenames where ((findItem NotCheckedOutFiles filename) == 0) collect filename if (existingFiles.count != 0) do ( retVal = run "reopen" existingFiles switches:#("-t", "+l") silent:silent ) ) return retVal ), -- Mark file(s) for deletion on Perforce: fn del filename silent:false = ( run "delete" filename silent:silent -- Delete existing file(s) if Perforce is disabled or files haven't been added to it: if not isKindOf filename array do ( filename = #(filename) ) for item in filename where doesFileExist item do ( setFileAttribute item #readOnly false deleteFile item ) ), fn revert filename unchangedOrUnsubmitted:false silent:false cl: = ( local optns = #() if unchangedOrUnsubmitted then ( append optns "-a" ) if unsupplied!=cl then ( append optns ("-c"+cl as string) ) if not isKindOf filename array do ( filename = #(filename) ) join optns filename run "revert" optns silent:silent ), -- Creates a new changelist, and returns the changelist number: fn createChangelist description silent:true = ( connect() -- If Perforce integration is turned off, skip this: if not RsProjectGetPerforceIntegration() do ( return undefined ) if not (connect()) do ( format "Perforce login failed.\n" return undefined ) outVal = this.findChangelistByName description if undefined!=outVal then ( gRsUlog.LogMessage ("Changelist with description already existent, going to append!") return outVal ) -- Can't use max string directly here local s = dotNetObject "System.String" description local outVal = p4.CreatePendingChangelist s if (outVal != undefined) do (outVal = outVal.number) if not silent do ( format "createChangelist: %\n" outVal ) return outVal ), -- Adds files to numbered changelist: fn addToChangelist listNum filename silent:true = ( if (listNum != undefined and listNum != unsupplied) then ( run "reopen" filename switches:#("-c", listNum as string) silent:silent ) else ( gRsUlog.LogWarning ("Undefined/Unsupplied listNum given to addToChangelist in p4.ms for filename: " + filename as string) ) ), fn addDeleteFilesToChangelist listNum filename silent:true = ( if (listNum != undefined and listNum != unsupplied) then ( run "delete" filename switches:#("-c", listNum as string) silent:silent ) else ( gRsUlog.LogWarning ("Undefined/Unsupplied listNum given to addDeleteFilesToChangelist in p4.ms for filename: " + filename as string) ) ), -- Finds pending changelist with description matching Description: fn findChangelistByName description silent:true = ( -- If Perforce integration is turned off, skip this: if not RsProjectGetPerforceIntegration() do ( return undefined ) if not gRsPerforce.connected() and not connect() then return undefined -- Get list of client's pending changelists: local changelists = run "changelists" #("-c", p4.client, "-s", "pending", "-l") returnResult:true silent:silent if (changelists == undefined) do (return false) -- Search for changelist with matching description += "\n" -- Descriptions have an extra linebreak local listNum for record in changelists.records while (listNum == undefined) do ( if (matchPattern record.item["desc"] pattern:description) do ( listNum = record.item["change"] ) ) return listNum ), -- Adds files to changelist matching the given description, creating a new one if necessary: fn addToChangelistByName description filename exclusive:false silent:true queue: = ( -- If Perforce integration is turned off, skip this: if not RsProjectGetPerforceIntegration() do ( return undefined ) local retVal = add_or_edit filename silent:silent queue:queue if not retVal do return false local addToListNum = findChangelistByName description silent:silent if (addToListNum == false) do (return false) if (addToListNum == undefined) do ( addToListNum = createChangelist description silent:silent ) if (addToListNum == undefined) do (return false) retVal = addToChangelist addToListNum filename silent:silent return retVal ), -- Sees if a given changelist has any files in it. If so, then it submits the changelist, otherwise it deletes it. fn submitOrDeleteChangelist changelistNum reOpen:false silent:false = ( local retVal = false if changelistNum != undefined do ( local clString = changelistNum as string pushPrompt ("Submitting changelist " + clString) -- Revert unchanged files before submitting: if not reOpen do ( run "revert" #("-a", "-c", clString) silent:true ) local p4Res = run "describe" clString returnResult:true silent:true -- Only submit if there are files to submit if( p4Res != undefined and p4Res.records.count>0 and ( p4Res.records[1].ArrayFields.Item["depotFile"] != undefined ) ) then ( local args = #("-c", clString) if reOpen do ( -- Reopen files after submit, and don't submit unchanged files: join args #("-r", "-f", "leaveunchanged") ) if not silent do ( format "Auto-checkin of changelist number: %\n" clString ) -- Submit changelist: retVal = run "submit" args silent:silent ) else if (p4Res != undefined and p4Res.records.count==0) then ( gRsUlog.LogMessage ("Trying to \"submitOrDeleteChangelist\" with undefined CL number \""+clString+"\"." ) retVal = true ) else ( -- Delete empty changelist retVal = run "change" #("-d", (changelistNum as string)) silent:true ) popPrompt() ) return retVal ), -- Sees if a given changelist has any changed files in it. If not then it is deleted. fn deleteChangelistIfItHasNoChanges changelistNum = ( local retVal = true if changelistNum != undefined do ( local clString = changelistNum as string pushPrompt ("Checking changelist " + clString) -- Revert unchanged files run "revert" #("-a", "-c", clString) silent:true local p4Res = run "describe" clString returnResult:true silent:true -- Only submit if there are files to submit if( p4Res == undefined or p4Res.records.count == 0 or ( p4Res.records[1].ArrayFields.Item["depotFile"] == undefined ) ) then ( -- Delete empty changelist retVal = run "change" #("-d", (changelistNum as string)) silent:true ) popPrompt() ) return retVal ), -- Returns the time this file was last submitted: fn fileSubmitTime filename silent:true doLogin:false = ( local fileInfo = run "fstat" filename returnResult:true silent:silent doLogin:doLogin if (fileInfo == undefined) do return undefined local fileRecord= fileInfo.records[1] if (fileRecord == undefined) do return undefined local headTime = fileRecord.Item["headTime"] if (headTime == undefined) do return undefined -- Convert time from unix-epoch to dotnet ticks: local headTimeTicks = Integer64 (((headTime as Integer64) * 10000000) + 621355968000000000) local dnHeadTime = dotNetObject "system.dateTime" headTimeTicks return dnHeadTime ), -------------------------------------------------------------- -- Resolves checked-out files in list, -- to show as latest versions: -------------------------------------------------------------- fn resolveToLatest filenames = ( if not (isKindOf filenames array) do (filenames = #(filenames)) local checkedOutNums = checkedOutForEdit filenames local checkedOutFiles = for fileNum = checkedOutNums collect filenames[fileNum] if (checkedOutFiles.count == 0) then ( return true ) else ( sync checkedOutFiles silent:true run "resolve" checkedOutFiles switches:#("-ay") silent:true ) ), -------------------------------------------------------------- -- Tells user if files are read-only or not checked out, -- and offers choice to check them out, -- make them writable, or cancel -------------------------------------------------------------- readOnlyP4Check_ignoreFiles = #(), fn readOnlyP4Check filenames exclusive:true queue: switches:#() includeWritable:false = ( local localFiles = for f in filenames where (RsFileExists f) collect f local defBtn = if (RsAutomatedExport == false) then 2 else 1 local tout = if (RsAutomatedExport == false) then 30 else 1 local strMissingLocally = "Files appear to be missing locally but are present in perforce.\nThis can cause issues when exporting.\nDo you want to force sync these files?:\n" local strWritable = "Files appear to be present locally but are marked as writable, so perforce cannot work with the file.\nDo you want to force sync and check out these files?\n***WARNING!*** This will clobber (overwrite) any local changes!" local useStr = strMissingLocally local useItems = filenames if(localFiles.count >= filenames.count) then ( useStr = strWritable useItems = for f in filenames where (not (getFileAttribute f #readOnly)) collect f ) if (RsQueryBoxMultiBtn useStr title:"File Query" timeout:tout labels:#("Yes", "No") defaultBtn:defBtn listItems:useItems width:500) == 1 then ( gRsPerforce.sync filenames force:true ) ::RsMapExportCancelled = false -- Filter out ignored filenames, but make sure they're writable: filenames = for filename in filenames collect ( if (findItem readOnlyP4Check_ignoreFiles filename) == 0 then filename else ( if doesFileExist filename do ( setFileAttribute filename #readOnly false ) dontCollect ) ) if (filenames.count == 0) do return true local retVal = true local notInP4Files = #() local filesNotCheckedOut = #() if RsProjectGetPerforceIntegration() then ( -- Check to see which files are already in Perforce: pushPrompt "Checking for existing Perforce files" local existingP4FileNums = exists filenames local existingP4Files = #() -- Check bitarray to see what files do exist in perforce for i = 1 to filenames.count do ( if existingP4FileNums[i] then ( append existingP4Files filenames[i] ) else ( append notInP4Files filenames[i] ) ) popPrompt() pushPrompt "Looking for checked-out files" local filesAlreadyCheckedOut = checkedOutForEdit existingP4Files -- Check writability of non-checked-out files: for i = 1 to existingP4Files.count where not filesAlreadyCheckedOut[i] do ( -- File isn't already checked out by the user: -- AJM: A flag can be passed now to get files that are writeable locally to -- still get included in the list of files that they don't have checked out. (.ped.zip files -- being requested for this). if (RsIsFileReadOnly existingP4Files[i] or includeWritable) do ( append filesNotCheckedOut existingP4Files[i] ) ) popPrompt() ) else ( -- If Perforce integration is turned off, make the export-files writable automatically: for filename in filenames where doesFileExist filename do ( setFileAttribute filename #readOnly false ) ) local hasNotInP4 = (notInP4Files.count != 0) local hasNotCheckedOut = (filesNotCheckedOut.count != 0) local filesCount = notInP4Files.count + filesNotCheckedOut.count -- Bring up query if any non-P4/readonly files were found: if hasNotInP4 or hasNotCheckedOut do ( local queryList = #() for filename in notInP4Files do ( append queryList #(filename, "not in Perforce") ) for filename in filesNotCheckedOut do ( append queryList #(filename, "not checked out") ) local btnLabels = #("Checkout", "Make Writable", "Cancel") local checkboxLabels = #() local checkboxStates = #{} if hasNotInP4 do ( btnLabels[1] = if (notInP4Files.count == filesCount) then "Add" else "Add/Checkout" local checkPlural = if (notInP4Files.count == 1) then "this file" else "these files" append checkboxLabels ("Don't ask to add " + checkPlural + " again") ) local queryMsg = stringStream "" format "%" (if (filesCount == 1) then "This file is not " else "These files are not ") to:queryMsg if hasNotInP4 do (format "in perforce" to:queryMsg) if (hasNotInP4 and hasNotCheckedOut) do (format "/" to:queryMsg) if hasNotCheckedOut do (format "checked out" to:queryMsg) format ".\n\nDo you want to fix this, or cancel export?" to:queryMsg local checkOutFiles local makeWritable local ignoreInFuture if not RsAutomatedExport then ( local queryVal = RsQueryBoxMultiBtn (queryMsg as string) title:"Build-file Perforce-check warnings" timeout:30 defaultBtn:1 listItems:queryList \ checkLabels:checkboxLabels checkStates:checkboxStates width:500 \ labels:btnLabels tooltips:#("Check files out of Perforce, or add them if they're new.", "Make files locally writable", "Cancel export") checkOutFiles = (queryVal == 1) makeWritable = (queryVal == 2) RsMapExportCancelled = (queryVal == 3) ignoreInFuture = checkboxStates[1] ) else ( -- If automated then make files writable rather than trying to checkout. -- Also saves putting up the dialog and then timing out waiting for next choice -- if the files are already checked out. checkOutFiles = false makeWritable = true ignoreInFuture = false ) if not RsMapExportCancelled do ( -- These files will no longer trigger "Add to Perforce?" warnings for this session: if ignoreInFuture do ( join readOnlyP4Check_ignoreFiles notInP4Files ) -- Set queueAdd to true if queue is specified: if (queue == unsupplied) then ( queue = addQueue ) if checkOutFiles and not RsAutomatedExport do ( local success = add_or_edit filenames exclusive:exclusive switches:switches queueAdd:true queue:queue -- If user currently has checked out files with non-latest local versions, resolve to the current versions: resolveToLatest filenames if not success do ( local message = "Perforce errors were encountered.\nDo you want to make the files locally writable, or cancel export?" local queryVal = RsQueryBoxMultiBtn message title:"Build-file Perforce-checkout errors" timeout:30 defaultBtn:2 \ labels:#("Make Writable", "Cancel") tooltips:#("Make files locally writable", "Cancel export") makeWritable = (queryVal == 1) if not makeWritable do (RsMapExportCancelled = true) ) ) if makeWritable do ( for filename in filenames where doesFileExist filename do ( setFileAttribute filename #readOnly false ) ) ) ) return retVal and (not RsMapExportCancelled) ), -------------------------------------------------------------- -- Adds filenames to new named changelist, -- then submits it -------------------------------------------------------------- fn submitFilesWithText filenames changeText silent:false queue: = ( pushPrompt "Creating new changelist..." add_Or_Edit filenames silent:silent queue:queue local changelistNum = createChangelist changeText silent:silent addToChangelist changelistNum filenames silent:silent local success = submitOrDeleteChangelist changelistNum silent:silent reOpen:true popPrompt() if not (success or silent) do ( messageBox ("Failed to submit changelist " + changelistNum as string + "\n\nPlease check this in your pending changes - perhaps a file needs to be resolved?") title:"Error: Changelist submission failure" ) return success ), -------------------------------------------------------------- -- Gets changelist-description from user, -- submits filenames to new changelist with that text -------------------------------------------------------------- fn submitFilesGetText filenames silent:false = ( newListText = filenames rollout RsP4GetText "Perforce Changelist Text" width:400 ( label lblChangeText "Edit changelist comment:" editText txtChangeText "" height:100 button btnOK "Okay" width:60 across:2 button btnCancel "Cancel" width:60 on RsP4GetText open do ( local msg = stringStream "" format "Submitting latest changes to: " to:msg for filename in ::gRsPerforce.newListText do ( format "% " (filenameFromPath filename) to:msg ) txtChangeText.text = msg as string ::gRsPerforce.newListText = undefined ) on btnOK pressed do ( ::gRsPerforce.newListText = txtChangeText.text destroyDialog RsP4GetText ) on btnCancel pressed do ( destroyDialog RsP4GetText ) ) createDialog RsP4GetText modal:true if (newListText == undefined) then ( return false ) else ( submitFilesWithText filenames newListText silent:silent ) ), -------------------------------------------------------------- -- Deletes empty changelists which match the wildcard description -- passed in. -------------------------------------------------------------- fn deleteEmptyChangelists description = ( if ( description == undefined ) do ( return false ) if (p4 == undefined) do ( connect() ) -- Find pending changelists for this user -- -l is used for full length changelist descriptions local p4RecordSet = run "changes" #("-l", "-s", "pending", "-u", p4.User) returnResult:true silent:true changeListNumbers = #() if ( p4RecordSet != undefined ) do ( for record in p4RecordSet.Records do ( if (matchPattern record.Item["desc"] pattern:description) do ( append changeListNumbers record.Item["change"] ) ) ) -- Find out about each changelist if ( changeListNumbers.count > 0 ) do ( local p4Res = run "describe" changeListNumbers returnResult:true silent:true if ( p4Res != undefined ) do ( for record in p4Res.records do ( -- Only delete if there are no files in the changelist if ( p4Res != undefined and ( record.ArrayFields.Item["depotFile"] == undefined ) ) then ( run "change" #("-d", record.Item["change"]) silent:true gRsULog.LogMessage ("Deleted old empty auto-changelist number: " + record.Item["change"]) ) ) ) ) ), -------------------------------------------------------------- -- ShouldGetHeadRevision files (= string or Array) -- Fires a query box and give the user the current revision and asks if they want the latest head revision of one or multiple files. -------------------------------------------------------------- fn ShouldGetHeadRevision files title:"File Query" =( if not RsProjectGetPerforceIntegration() do return false local fname = #() if (isKindOf files string) then ( if (files != "") then (append fname files) )else if (isKindOf files Array) then ( fname = for f in files where (f != "") collect f ) if(fname.count == 0) then( gRsUlog.LogWarning "gRsPerforce.ShouldGetHeadRevision called with no files in file list" return false ) local fstats = ( gRsPerforce.getFileStats fname ) local outofdate = #() local outofdateFileNamesToSync = #() for i = 1 to fstats.count do( local haveRev = fstats[i].Item["haveRev"] local headRev = fstats[i].Item["headRev"] if (haveRev != undefined) then (haveRev = haveRev as integer) else continue if (headRev != undefined) then (headRev = headRev as integer) else continue if haveRev < headRev then ( append outofdate #((RsMakeSafeSlashes fname[i]),haveRev,headRev) append outofdateFileNamesToSync (RsMakeSafeSlashes fname[i]) ) ) local sPlural = if (outofdate.count == 1) then "" else "s" local arePlural = if (outofdate.count == 1) then "is" else "are" local thesePlural = if (outofdate.count == 1) then "this" else "these" if (outofdate.count == 0 ) then ( msg = stringstream "" format "No file% % out of date\n" sPlural arePlural to:msg gRsUlog.LogMessage msg return false ) ss = stringstream "" format "There % % file% which % out of date. Would you like to sync % file%?" arePlural outofdate.count sPlural arePlural thesePlural sPlural to:ss if (RsQueryBoxMultiBtn (ss as string) title:title timeout:20 labels:#("Yes", "No") defaultBtn:2 listTitles:#("File","Current Revision","Head revision") listItems:outofdate width:800) == 1 then ( gRsPerforce.sync outofdateFileNamesToSync ) ), -------------------------------------------------------------- -- Returns either: -- An array of FileMapping dotNetObjects if an array is supplied -- OR -- A single FileMapping dotNetObject if a single string supplied -------------------------------------------------------------- fn getFileMapping files = ( local returnVal = undefined if ( fileMapping == undefined) then fileMapping = dotNetClass "RSG.SourceControl.Perforce.FileMapping" if(isKindOf files Array) then (returnVal = fileMapping.Create p4 files) else (returnVal = (fileMapping.Create p4 #(files))[1]) returnVal ), -------------------------------------------------------------- -- Returns either a string or an array depending on what was passed. -- Will match the number of array elements supplied whether they exist in p4 or not for easy matching whether files exist e.g. -- Passing: -- #("//depot/gta5/blah/blah/fake/dir/MadeUpName.bat", "X:\gta5\assets_ng\export\data\peds.pso.meta") -- will return -- #(undefined, "//depot/gta5/assets_ng/export/data/peds.pso.meta") -- -- Usage -- gRsPerforce.FileMapTo #depot @"x:\gta5\assets_ng\export\levels\gta5\_cityw\beverly_01\bh1_01_props.xml" -- gRsPerforce.FileMapTo #local "//depot/gta5/assets_ng/export/levels/gta5/_cityw/beverly_01/bh1_01_props.xml" -- -------------------------------------------------------------- fn FileMapTo mapTo fname = ( if (not connected()) then connect() if(fileMapping == undefined) then return undefined local fMap = undefined local returnVal = undefined if(isKindOf fname Array) then ( returnVal = #() -- Fix all the paths in array local slash = "/" if(mapTo as name) == #depot do( slash = "" ) local tmpArr = #() for f in fname do( append tmpArr ( slash + (RsMakeSafeSlashes f)) ) -- clear original fname = #() join fname tmpArr try( fMap = (getFileMapping fname) )catch( gRsULog.LogError("Exception: " + getCurrentException()) return undefined ) localNameArray = #() depotNameArray = #() if(fMap != undefined) then ( for i in fMap do( append depotNameArray (i.DepotFilename) append localNameArray (i.LocalFilename) ) )else( local arr = #() -- try to pass an array of the same size back. for i = 1 to fname.count do( append arr undefined ) ) -- get bitarray of existing files. bits = exists localNameArray -- Append only existing files. for f = 1 to bits.count do( if(bits[f] == true) then if(mapTo as Name) == #depot then (append returnVal depotNameArray[f]) else (append returnVal localNameArray[f]) else (append returnVal undefined) ) )else( local slash = "/" if(mapTo as name) == #depot do( slash = "" ) fname = slash + (RsMakeSafeSlashes fname) try( fMap = getFileMapping fname )catch( format "Exception: %\n" getCurrentException() return undefined ) if(fMap != undefined) then ( local bits = exists(#(fMap.LocalFilename)) if(bits[1] == true) then returnVal = if(mapTo as name) == #depot then fMap.DepotFilename else fMap.LocalFilename else returnVal = undefined )else( returnVal = undefined ) ) returnVal ), -------------------------------------------------------------- -- Replacement for old function using the new FileMapTo function -------------------------------------------------------------- fn local2depot fname = ( FileMapTo #depot fname ), -------------------------------------------------------------- -- Unused at the moment but put in anyway. -------------------------------------------------------------- fn depot2local fname = ( FileMapTo #local fname ) ) fn ShutdownP4 = ( try ( gRsPerforce.p4.dispose() --print "Disposing current Perforce object..." ) catch () ) -- Make sure we clear our last instance of the P4 object otherwise we leak as bad as my kitchen sink does. A bucket won't help this scenario though! if (gRsPerforce != undefined) then ShutdownP4() gRsPerforce = RsPerforce() callbacks.removeScripts id:#RsP4Callbacks callbacks.addScript #preSystemShutdown "ShutdownP4()" id:#RsP4Callbacks -- pipeline/util/p4.ms