public static bool ShowModal(IWin32Window Owner, PerforceConnection DefaultConnection, string StreamName, string ProjectPath, int Changelist, string Command, UserSettings Settings, TextWriter Log, out BuildInfo BuildInfo) { string DefaultWorkspaceName = AutomatedSyncWindow.FindDefaultWorkspace(Owner, DefaultConnection, StreamName, Log); string DefaultProjectPath = null; if (!String.IsNullOrEmpty(ProjectPath)) { DefaultProjectPath = ProjectPath; } else if (DefaultWorkspaceName != null) { string ClientPrefix = String.Format("//{0}/", DefaultWorkspaceName); foreach (UserSelectedProjectSettings ProjectSettings in Settings.RecentProjects) { if (ProjectSettings.ClientPath.StartsWith(ClientPrefix, StringComparison.OrdinalIgnoreCase)) { DefaultProjectPath = ProjectSettings.ClientPath.Substring(ClientPrefix.Length - 1); break; } } } AutomatedBuildWindow Window = new AutomatedBuildWindow(StreamName, Changelist, Command, DefaultConnection, DefaultWorkspaceName, DefaultProjectPath, Log); if (Window.ShowDialog() == DialogResult.OK) { BuildInfo = Window.Result; return(true); } else { BuildInfo = null; return(false); } }
/// <summary> /// Filters all the likely causers from the list of changes since an issue was created /// </summary> /// <param name="Perforce">The perforce connection</param> /// <param name="Fingerprint">Fingerprint for the issue</param> /// <param name="Changes">List of changes since the issue first occurred.</param> /// <returns>List of changes which are causers for the issue</returns> public virtual List <ChangeInfo> FindCausers(PerforceConnection Perforce, BuildHealthIssue Issue, IReadOnlyList <ChangeInfo> Changes) { SortedSet <string> FileNamesWithoutPath = GetFileNamesWithoutPath(Issue.FileNames); List <ChangeInfo> Causers = new List <ChangeInfo>(); foreach (ChangeInfo Change in Changes) { DescribeRecord Description = Perforce.Describe(Change.Record.Number).Data; if (ContainsFileNames(Description, FileNamesWithoutPath)) { Causers.Add(Change); } } if (Causers.Count > 0) { return(Causers); } else { return(new List <ChangeInfo>(Changes)); } }
/// <summary> /// Finds or adds an issue for a particular issue /// </summary> /// <param name="Perforce">Perforce connection used to find possible causers</param> /// <param name="State">The current set of tracked issues</param> /// <param name="Build">The new build</param> /// <param name="PreviousChange">The last changelist that was built before this one</param> /// <param name="InputJob">Job containing the step to add</param> /// <param name="InputJobStep">The job step to add</param> BuildHealthIssue MergeIntoExistingIssue(PerforceConnection Perforce, BuildHealthState State, InputJob InputJob, InputJobStep InputJobStep, BuildHealthIssue InputIssue, Lazy <IReadOnlyList <ChangeInfo> > LazyChanges) { // Find the pattern matcher for this fingerprint PatternMatcher Matcher = CategoryToMatcher[InputIssue.Category]; // Check if it can be added to an existing open issue foreach (BuildHealthIssue Issue in State.Issues) { // Check this issue already exists in the current stream Dictionary <string, BuildHealthJobHistory> StepNameToHistory; if (!Issue.Streams.TryGetValue(InputJob.Stream, out StepNameToHistory)) { continue; } // Check that this issue has not already been closed BuildHealthJobHistory History; if (StepNameToHistory.TryGetValue(InputJobStep.Name, out History)) { if (!History.CanAddFailedBuild(InputJob.Change)) { continue; } } else { if (!StepNameToHistory.Values.Any(x => x.CanAddFailedBuild(InputJob.Change))) { continue; } } // Try to merge the fingerprint if (!Matcher.CanMerge(InputIssue, Issue)) { continue; } // Add the new build Matcher.Merge(InputIssue, Issue); return(Issue); } // Check if this issue can be merged with an issue built in another stream IReadOnlyList <ChangeInfo> Changes = LazyChanges.Value; if (Changes != null && Changes.Count > 0) { SortedSet <int> SourceChanges = new SortedSet <int>(Changes.SelectMany(x => x.SourceChanges)); foreach (BuildHealthIssue Issue in State.Issues) { // Check if this issue does not already contain this stream, but contains one of the causing changes if (Issue.Streams.ContainsKey(InputJob.Stream)) { continue; } if (!SourceChanges.Any(x => Issue.SourceChanges.Contains(x))) { continue; } if (!Matcher.CanMerge(InputIssue, Issue)) { continue; } // Merge the issue Matcher.Merge(InputIssue, Issue); return(Issue); } } // Check if it can be merged into an issue that's been created for this job. We only do this after exhausting all other options. foreach (BuildHealthIssue Issue in State.Issues) { if (Issue.InitialJobUrl == InputIssue.InitialJobUrl && Matcher.CanMergeInitialJob(InputIssue, Issue)) { Matcher.Merge(InputIssue, Issue); return(Issue); } } return(null); }
/// <summary> /// Main command entry point /// </summary> /// <param name="Arguments">The command line arguments</param> public override void Exec(CommandLineArguments Arguments) { // Parse the arguments bool bClean = Arguments.HasOption("-Clean"); string PerforcePort = Arguments.GetStringOrDefault("-P4Port=", null); string PerforceUser = Arguments.GetStringOrDefault("-P4User="******"-InputFile=", null); FileReference StateFile = Arguments.GetFileReference("-StateFile="); string ServerUrl = Arguments.GetStringOrDefault("-Server=", null); bool bKeepHistory = Arguments.HasOption("-KeepHistory"); bool bReadOnly = Arguments.HasOption("-ReadOnly"); DirectoryReference SaveUnmatchedDir = Arguments.GetDirectoryReferenceOrDefault("-SaveUnmatched=", null); Arguments.CheckAllArgumentsUsed(); // Build a mapping from category to matching Dictionary <string, PatternMatcher> CategoryNameToMatcher = new Dictionary <string, PatternMatcher>(); foreach (PatternMatcher Matcher in Matchers) { CategoryNameToMatcher[Matcher.Category] = Matcher; } // Complete any interrupted operation to update the state file CompleteStateTransaction(StateFile); // Read the persistent data file BuildHealthState State; if (!bClean && FileReference.Exists(StateFile)) { Log.TraceInformation("Reading persistent data from {0}", StateFile); State = DeserializeJson <BuildHealthState>(StateFile); } else { Log.TraceInformation("Creating new persistent data"); State = new BuildHealthState(); } // Fixup any issues loaded from disk foreach (BuildHealthIssue Issue in State.Issues) { if (Issue.References == null) { Issue.References = new SortedSet <string>(); } } // Create the Perforce connection PerforceConnection Perforce = new PerforceConnection(PerforcePort, PerforceUser, null); // Process the input data if (InputFile != null) { // Parse the input file Log.TraceInformation("Reading build results from {0}", InputFile); InputData InputData = DeserializeJson <InputData>(InputFile); // Parse all the builds and add them to the persistent data List <InputJob> InputJobs = InputData.Jobs.OrderBy(x => x.Change).ThenBy(x => x.Stream).ToList(); Stopwatch Timer = Stopwatch.StartNew(); foreach (InputJob InputJob in InputJobs) { // Add a new build for each job step foreach (InputJobStep InputJobStep in InputJob.Steps) { BuildHealthJobStep NewBuild = new BuildHealthJobStep(InputJob.Change, InputJob.Name, InputJob.Url, InputJobStep.Name, InputJobStep.Url, null); State.AddBuild(InputJob.Stream, NewBuild); } // Add all the job steps List <InputJobStep> InputJobSteps = InputJob.Steps.OrderBy(x => x.Name).ToList(); foreach (InputJobStep InputJobStep in InputJobSteps) { if (InputJobStep.Diagnostics != null && InputJobStep.Diagnostics.Count > 0) { AddStep(Perforce, State, InputJob, InputJobStep); } } // Remove any steps which are empty InputJob.Steps.RemoveAll(x => x.Diagnostics == null || x.Diagnostics.Count == 0); } InputJobs.RemoveAll(x => x.Steps.Count == 0); Log.TraceInformation("Added jobs in {0}s", Timer.Elapsed.TotalSeconds); // If there are any unmatched issues, save out the current state and remaining input if (SaveUnmatchedDir != null && InputJobs.Count > 0) { DirectoryReference.CreateDirectory(SaveUnmatchedDir); if (FileReference.Exists(StateFile)) { FileReference.Copy(StateFile, FileReference.Combine(SaveUnmatchedDir, "State.json"), true); } SerializeJson(FileReference.Combine(SaveUnmatchedDir, "Input.json"), InputData); } // Try to find the next successful build for each stream, so we can close it as part of updating the server for (int Idx = 0; Idx < State.Issues.Count; Idx++) { BuildHealthIssue Issue = State.Issues[Idx]; foreach (string Stream in Issue.Streams.Keys) { Dictionary <string, BuildHealthJobHistory> StepNameToHistory = Issue.Streams[Stream]; foreach (string StepName in StepNameToHistory.Keys) { BuildHealthJobHistory IssueHistory = StepNameToHistory[StepName]; if (IssueHistory.FailedBuilds.Count > 0 && IssueHistory.NextSuccessfulBuild == null) { // Find the successful build after this change BuildHealthJobStep LastFailedBuild = IssueHistory.FailedBuilds[IssueHistory.FailedBuilds.Count - 1]; IssueHistory.NextSuccessfulBuild = State.FindBuildAfter(Stream, LastFailedBuild.Change, StepName); } } } } // Find the change two days before the latest change being added if (InputData.Jobs.Count > 0 && !bKeepHistory) { // Find all the unique change numbers for each stream SortedSet <int> ChangeNumbers = new SortedSet <int>(); foreach (List <BuildHealthJobStep> Builds in State.Streams.Values) { ChangeNumbers.UnionWith(Builds.Select(x => x.Change)); } // Get the latest change record int LatestChangeNumber = InputData.Jobs.Min(x => x.Change); ChangeRecord LatestChangeRecord = Perforce.GetChange(GetChangeOptions.None, LatestChangeNumber).Data; // Step forward through all the changelists until we get to one we don't want to delete int DeleteChangeNumber = -1; foreach (int ChangeNumber in ChangeNumbers) { ChangeRecord ChangeRecord = Perforce.GetChange(GetChangeOptions.None, ChangeNumber).Data; if (ChangeRecord.Date > LatestChangeRecord.Date - TimeSpan.FromDays(2)) { break; } DeleteChangeNumber = ChangeNumber; } // Remove any builds we no longer want to track foreach (List <BuildHealthJobStep> Builds in State.Streams.Values) { Builds.RemoveAll(x => x.Change <= DeleteChangeNumber); } } } // Mark any issues as resolved foreach (BuildHealthIssue Issue in State.Issues) { if (Issue.IsResolved()) { if (!Issue.ResolvedAt.HasValue) { Issue.ResolvedAt = DateTime.UtcNow; } } else { if (Issue.ResolvedAt.HasValue) { Issue.ResolvedAt = null; } } } // If we're in read-only mode, don't write anything out if (bReadOnly) { return; } // Save the persistent data Log.TraceInformation("Writing persistent data to {0}", StateFile); DirectoryReference.CreateDirectory(StateFile.Directory); WriteState(StateFile, State); // Synchronize with the server if (ServerUrl != null) { // Post any issue updates foreach (BuildHealthIssue Issue in State.Issues) { PatternMatcher Matcher; if (!CategoryNameToMatcher.TryGetValue(Issue.Category, out Matcher)) { continue; } string Summary = Matcher.GetSummary(Issue); if (Issue.Id == -1) { Log.TraceInformation("Adding issue: {0}", Issue); if (Issue.PendingWatchers.Count == 0) { Log.TraceWarning("(No possible causers)"); } CommandTypes.AddIssue IssueBody = new CommandTypes.AddIssue(); IssueBody.Project = Issue.Project; IssueBody.Summary = Summary; if (Issue.PendingWatchers.Count == 1) { IssueBody.Owner = Issue.PendingWatchers.First(); } using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues", ServerUrl), "POST", IssueBody)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to add issue"); } Issue.Id = ParseHttpResponse <CommandTypes.AddIssueResponse>(Response).Id; } Issue.PostedSummary = Summary; WriteState(StateFile, State); } else if (Issue.PostedSummary == null || !String.Equals(Issue.PostedSummary, Summary, StringComparison.Ordinal)) { Log.TraceInformation("Updating issue {0}", Issue.Id); CommandTypes.UpdateIssue IssueBody = new CommandTypes.UpdateIssue(); IssueBody.Summary = Summary; using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}", ServerUrl, Issue.Id), "PUT", IssueBody)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to add issue"); } } Issue.PostedSummary = Summary; WriteState(StateFile, State); } } // Add any new builds associated with issues Dictionary <string, long> JobStepUrlToId = new Dictionary <string, long>(StringComparer.Ordinal); foreach (BuildHealthIssue Issue in State.Issues) { foreach (KeyValuePair <string, Dictionary <string, BuildHealthJobHistory> > StreamPair in Issue.Streams) { foreach (BuildHealthJobHistory StreamHistory in StreamPair.Value.Values) { foreach (BuildHealthJobStep Build in StreamHistory.Builds) { if (!Build.bPostedToServer) { Log.TraceInformation("Adding {0} to issue {1}", Build.JobStepUrl, Issue.Id); CommandTypes.AddBuild AddBuild = new CommandTypes.AddBuild(); AddBuild.Stream = StreamPair.Key; AddBuild.Change = Build.Change; AddBuild.JobName = Build.JobName; AddBuild.JobUrl = Build.JobUrl; AddBuild.JobStepName = Build.JobStepName; AddBuild.JobStepUrl = Build.JobStepUrl; AddBuild.ErrorUrl = Build.ErrorUrl; AddBuild.Outcome = (Build == StreamHistory.PrevSuccessfulBuild || Build == StreamHistory.NextSuccessfulBuild)? CommandTypes.Outcome.Success : CommandTypes.Outcome.Error; using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/builds", ServerUrl, Issue.Id), "POST", AddBuild)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to add build"); } Build.Id = ParseHttpResponse <CommandTypes.AddBuildResponse>(Response).Id; } Build.bPostedToServer = true; WriteState(StateFile, State); } if (Build.Id != -1) { JobStepUrlToId[Build.JobStepUrl] = Build.Id; } } } } } // Add any new diagnostics foreach (BuildHealthIssue Issue in State.Issues) { foreach (BuildHealthDiagnostic Diagnostic in Issue.Diagnostics) { if (!Diagnostic.bPostedToServer) { string Summary = Diagnostic.Message; const int MaxLength = 40; if (Summary.Length > MaxLength) { Summary = Summary.Substring(0, MaxLength).TrimEnd(); } Log.TraceInformation("Adding diagnostic '{0}' to issue {1}", Summary, Issue.Id); CommandTypes.AddDiagnostic AddDiagnostic = new CommandTypes.AddDiagnostic(); long BuildId; if (Diagnostic.JobStepUrl != null && JobStepUrlToId.TryGetValue(Diagnostic.JobStepUrl, out BuildId)) { AddDiagnostic.BuildId = BuildId; } else { Console.WriteLine("ERROR"); } AddDiagnostic.Message = Diagnostic.Message; AddDiagnostic.Url = Diagnostic.ErrorUrl; using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/diagnostics", ServerUrl, Issue.Id), "POST", AddDiagnostic)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to add build"); } } Diagnostic.bPostedToServer = true; WriteState(StateFile, State); } } } // Close any issues which are complete for (int Idx = 0; Idx < State.Issues.Count; Idx++) { BuildHealthIssue Issue = State.Issues[Idx]; if (Issue.ResolvedAt.HasValue != Issue.bPostedResolved) { Log.TraceInformation("Setting issue {0} resolved flag to {1}", Issue.Id, Issue.ResolvedAt.HasValue); CommandTypes.UpdateIssue UpdateBody = new CommandTypes.UpdateIssue(); UpdateBody.Resolved = Issue.ResolvedAt.HasValue; using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}", ServerUrl, Issue.Id), "PUT", UpdateBody)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to delete issue"); } } Issue.bPostedResolved = Issue.ResolvedAt.HasValue; WriteState(StateFile, State); } } // Update watchers on any open builds foreach (BuildHealthIssue Issue in State.Issues) { while (Issue.PendingWatchers.Count > 0) { CommandTypes.Watcher Watcher = new CommandTypes.Watcher(); Watcher.UserName = Issue.PendingWatchers.First(); using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/watchers", ServerUrl, Issue.Id), "POST", Watcher)) { int ResponseCode = (int)Response.StatusCode; if (!(ResponseCode >= 200 && ResponseCode <= 299)) { throw new Exception("Unable to add watcher"); } } Issue.PendingWatchers.Remove(Watcher.UserName); Issue.Watchers.Add(Watcher.UserName); WriteState(StateFile, State); } } } // Remove any issues which have been resolved for 24 hours. We have to keep information about issues that have been fixed for some time; we may be updating the same job // multiple times while other steps are running, and we don't want to keep opening new issues for it. Also, it can take time for changes to propagate between streams. DateTime RemoveIssueTime = DateTime.UtcNow - TimeSpan.FromHours(24.0); for (int Idx = 0; Idx < State.Issues.Count; Idx++) { BuildHealthIssue Issue = State.Issues[Idx]; if (Issue.ResolvedAt.HasValue && Issue.ResolvedAt.Value < RemoveIssueTime) { State.Issues.RemoveAt(Idx--); WriteState(StateFile, State); continue; } } // TODO: VERIFY ISSUES ARE CLOSED }
public static bool SyncAndRun(PerforceConnection Perforce, string BaseDepotPath, bool bUnstable, string[] Args, Mutex InstanceMutex, TextWriter LogWriter) { try { string SyncPath = BaseDepotPath.TrimEnd('/') + (bUnstable? "/UnstableRelease/..." : "/Release/..."); LogWriter.WriteLine("Syncing from {0}", SyncPath); // Create the target folder string ApplicationFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UnrealGameSync", "Latest"); if (!SafeCreateDirectory(ApplicationFolder)) { LogWriter.WriteLine("Couldn't create directory: {0}", ApplicationFolder); return(false); } // Find the most recent changelist List <PerforceChangeSummary> Changes; if (!Perforce.FindChanges(SyncPath, 1, out Changes, LogWriter) || Changes.Count < 1) { LogWriter.WriteLine("Couldn't find last changelist"); return(false); } // Take the first changelist number int RequiredChangeNumber = Changes[0].Number; // Read the current version string SyncVersionFile = Path.Combine(ApplicationFolder, "SyncVersion.txt"); string RequiredSyncText = String.Format("{0}\n{1}@{2}", Perforce.ServerAndPort ?? "", SyncPath, RequiredChangeNumber); // Check the application exists string ApplicationExe = Path.Combine(ApplicationFolder, "UnrealGameSync.exe"); // Check if the version has changed string SyncText; if (!File.Exists(SyncVersionFile) || !File.Exists(ApplicationExe) || !TryReadAllText(SyncVersionFile, out SyncText) || SyncText != RequiredSyncText) { // Try to delete the directory contents. Retry for a while, in case we've been spawned by an application in this folder to do an update. for (int NumRetries = 0; !SafeDeleteDirectoryContents(ApplicationFolder); NumRetries++) { if (NumRetries > 20) { LogWriter.WriteLine("Couldn't delete contents of {0} (retried {1} times).", ApplicationFolder, NumRetries); return(false); } Thread.Sleep(500); } // Find all the files in the sync path at this changelist List <PerforceFileRecord> FileRecords; if (!Perforce.Stat(String.Format("{0}@{1}", SyncPath, RequiredChangeNumber), out FileRecords, LogWriter)) { LogWriter.WriteLine("Couldn't find matching files."); return(false); } // Sync all the files in this list to the same directory structure under the application folder string DepotPathPrefix = SyncPath.Substring(0, SyncPath.LastIndexOf('/') + 1); foreach (PerforceFileRecord FileRecord in FileRecords) { string LocalPath = Path.Combine(ApplicationFolder, FileRecord.DepotPath.Substring(DepotPathPrefix.Length).Replace('/', Path.DirectorySeparatorChar)); if (!SafeCreateDirectory(Path.GetDirectoryName(LocalPath))) { LogWriter.WriteLine("Couldn't create folder {0}", Path.GetDirectoryName(LocalPath)); return(false); } if (!Perforce.PrintToFile(FileRecord.DepotPath, LocalPath, LogWriter)) { LogWriter.WriteLine("Couldn't sync {0} to {1}", FileRecord.DepotPath, LocalPath); return(false); } } // Check the application exists if (!File.Exists(ApplicationExe)) { LogWriter.WriteLine("Application was not synced from Perforce. Check that UnrealGameSync exists at {0}/UnrealGameSync.exe, and you have access to it.", SyncPath); return(false); } // Update the version if (!TryWriteAllText(SyncVersionFile, RequiredSyncText)) { LogWriter.WriteLine("Couldn't write sync text to {0}", SyncVersionFile); return(false); } } LogWriter.WriteLine(); // Build the command line for the synced application, including the sync path to monitor for updates StringBuilder NewCommandLine = new StringBuilder(String.Format("-updatepath=\"{0}@>{1}\" -updatespawn=\"{2}\"{3}", SyncPath, RequiredChangeNumber, Assembly.GetEntryAssembly().Location, bUnstable? " -unstable" : "")); if (!String.IsNullOrEmpty(Perforce.ServerAndPort)) { NewCommandLine.AppendFormat(" -p4port={0}", QuoteArgument(Perforce.ServerAndPort)); } if (!String.IsNullOrEmpty(Perforce.UserName)) { NewCommandLine.AppendFormat(" -p4user={0}", QuoteArgument(Perforce.UserName)); } foreach (string Arg in Args) { NewCommandLine.AppendFormat(" {0}", QuoteArgument(Arg)); } // Release the mutex now so that the new application can start up InstanceMutex.Close(); // Spawn the application LogWriter.WriteLine("Spawning {0} with command line: {1}", ApplicationExe, NewCommandLine.ToString()); using (Process ChildProcess = new Process()) { ChildProcess.StartInfo.FileName = ApplicationExe; ChildProcess.StartInfo.Arguments = NewCommandLine.ToString(); ChildProcess.StartInfo.UseShellExecute = false; ChildProcess.StartInfo.CreateNoWindow = false; if (!ChildProcess.Start()) { LogWriter.WriteLine("Failed to start process"); return(false); } } return(true); } catch (Exception Ex) { LogWriter.WriteLine(Ex.ToString()); return(false); } }
private AutomatedBuildWindow(string StreamName, int Changelist, string Command, PerforceConnection DefaultConnection, string DefaultWorkspaceName, string DefaultProjectPath, TextWriter Log) { this.StreamName = StreamName; this.DefaultConnection = DefaultConnection; this.Log = Log; InitializeComponent(); ActiveControl = WorkspaceNameTextBox; MinimumSize = Size; MaximumSize = new Size(32768, Size.Height); SyncToChangeCheckBox.Text = String.Format("Sync to changelist {0}", Changelist); ExecCommandTextBox.Text = Command; if (DefaultWorkspaceName != null) { WorkspaceNameTextBox.Text = DefaultWorkspaceName; WorkspaceNameTextBox.Select(WorkspaceNameTextBox.Text.Length, 0); } if (DefaultProjectPath != null) { WorkspacePathTextBox.Text = DefaultProjectPath; WorkspacePathTextBox.Select(WorkspacePathTextBox.Text.Length, 0); } UpdateServerLabel(); UpdateOkButton(); UpdateWorkspacePathBrowseButton(); }
public override List <ChangeInfo> FindCausers(PerforceConnection Perforce, Issue Issue, IReadOnlyList <ChangeInfo> Changes) { return(new List <ChangeInfo>()); }
/// <summary> /// Finds or adds an issue for a particular fingerprint /// </summary> /// <param name="Perforce">Perforce connection used to find possible causers</param> /// <param name="State">The current set of tracked issues</param> /// <param name="Build">The new build</param> /// <param name="PreviousChange">The last changelist that was built before this one</param> /// <param name="InputJob">Job containing the step to add</param> /// <param name="InputJobStep">The job step to add</param> BuildHealthIssue FindOrAddIssueForFingerprint(PerforceConnection Perforce, BuildHealthState State, InputJob InputJob, InputJobStep InputJobStep, BuildHealthIssue InputIssue) { // Find the pattern matcher for this fingerprint PatternMatcher Matcher = CategoryToMatcher[InputIssue.Category]; // Check if it can be added to an existing open issue foreach (BuildHealthIssue Issue in State.Issues) { // Check this issue already exists in the current stream Dictionary <string, BuildHealthJobHistory> StepNameToHistory; if (!Issue.Streams.TryGetValue(InputJob.Stream, out StepNameToHistory)) { continue; } // Check that this issue has not already been closed BuildHealthJobHistory History; if (StepNameToHistory.TryGetValue(InputJobStep.Name, out History)) { if (!History.CanAddFailedBuild(InputJob.Change)) { continue; } } else { if (!StepNameToHistory.Values.Any(x => x.CanAddFailedBuild(InputJob.Change))) { continue; } } // Try to merge the fingerprint if (!Matcher.CanMerge(InputIssue, Issue)) { continue; } // Add the new build Matcher.Merge(InputIssue, Issue); return(Issue); } // List of changes since the last successful build in this stream IReadOnlyList <ChangeInfo> Changes = null; // Find the previous changelist that was built in this stream List <BuildHealthJobStep> StreamBuilds; if (State.Streams.TryGetValue(InputJob.Stream, out StreamBuilds)) { // Find the last change submitted to this stream before it started failing int LastChange = -1; for (int Idx = 0; Idx < StreamBuilds.Count && StreamBuilds[Idx].Change < InputJob.Change; Idx++) { LastChange = StreamBuilds[Idx].Change; } // Allow adding to any open issue that contains changes merged from other branches if (LastChange != -1) { // Query for all the changes since then Changes = FindChanges(Perforce, InputJob.Stream, LastChange, InputJob.Change); if (Changes.Count > 0) { SortedSet <int> SourceChanges = new SortedSet <int>(Changes.SelectMany(x => x.SourceChanges)); foreach (BuildHealthIssue Issue in State.Issues) { // Check if this issue does not already contain this stream, but contains one of the causing changes if (Issue.Streams.ContainsKey(InputJob.Stream)) { continue; } if (!SourceChanges.Any(x => Issue.SourceChanges.Contains(x))) { continue; } if (!Matcher.CanMerge(InputIssue, Issue)) { continue; } // Merge the issue Matcher.Merge(InputIssue, Issue); return(Issue); } } } } // Create new issues for everything else in this stream if (Changes != null) { List <ChangeInfo> Causers = Matcher.FindCausers(Perforce, InputIssue, Changes); foreach (ChangeInfo Causer in Causers) { InputIssue.SourceChanges.UnionWith(Causer.SourceChanges); InputIssue.PendingWatchers.Add(Causer.Record.User); } } State.Issues.Add(InputIssue); return(InputIssue); }