using System; using System.Text; using System.Linq; using System.Threading.Tasks; using KSU.CS.Pendant.Server.Models; using Microsoft.Extensions.Configuration; using System.IO; using LibGit2Sharp; using System.IO.Compression; namespace KSU.CS.Pendant.Server.Services { public class AssignmentValidationService : IAssignmentValidationService { private readonly DataContext _context; private readonly string _rootPath; public AssignmentValidationService(DataContext context, IConfiguration configuration) { _context = context; _rootPath = configuration["Paths:HomeDir"]; } #region Interface Methods /// /// Checks the assignment for the , /// generating a new Validation that describes what work still needs to be completed. /// /// The user whose milestone submission should be checked /// The milestone the submission is for public int Check(ValidationAttempt attempt) { return CheckAsync(attempt).GetAwaiter().GetResult(); } /// /// Checks the assignment for the , /// generating a new Validation that describes what work still needs to be completed. /// /// The user whose milestone submission should be checked /// The milestone the submission is for public async Task CheckAsync(ValidationAttempt attempt) { // Validate the assignment var result = await ProgramVerifier.Verifier.Check(attempt.SolutionPath, attempt.Assignment.SpecificationPath); // Populate the ValidationAttempt attempt.DateTime = DateTime.Now; attempt.StructuralIssues = result.StructuralIssues.Select(issue => new ValidationIssue() { Message = issue // TODO: add assignment url for HelpUrl }).ToList(); attempt.FunctionalIssues = result.FunctionalIssues.Select(issue => new ValidationIssue() { Message = issue }).ToList(); attempt.DesignIssues = result.DesignIssues.Select(issue => new ValidationIssue() { Message = issue.Message, HelpUrl = issue.HelpUrl, SourceUrl = BuildGitHubSourceUrl(attempt.RepoUrl, attempt.CommitIdentifier, issue.Path, issue.Line) }).ToList(); attempt.StyleIssues = result.StyleIssues.Select(issue => new ValidationIssue() { Message = issue.Message, HelpUrl = issue.HelpUrl, SourceUrl = BuildGitHubSourceUrl(attempt.RepoUrl, attempt.CommitIdentifier, issue.Path, issue.Line) }).ToList(); _context.ValidationAttempts.Add(attempt); await _context.SaveChangesAsync(); return attempt.IssueCount; } /// /// Checks the assignment for the , /// generating a new Validation that describes what work still needs to be completed. /// /// The user whose milestone submission should be checked /// The milestone the submission is for public void CheckMilestone(User user, MilestoneAssignment milestone) { CheckMilestoneAsync(user, milestone).GetAwaiter().GetResult(); } /// /// Checks the assignment for the , /// generating a new Validation that describes what work still needs to be completed. /// /// The user whose milestone submission should be checked /// The milestone the submission is for public async Task CheckMilestoneAsync(User user, MilestoneAssignment milestone) { var ownerName = milestone.IterativeProject.GitHubOrganizationName; var repoName = $"{milestone.IterativeProject.RepositoryPrefix}-{user.GitHubAccount.Username}"; var branchName = milestone.BranchName; // Clone the repo var studentPath = CloneAndCheckout(user, milestone, ownerName, repoName, branchName); // Validate the assignment var result = await ProgramVerifier.Verifier.Check(studentPath, milestone.SpecificationPath); // Create the ValidationAttempt var attempt = new ValidationAttempt() { User = user, Assignment = milestone, SolutionPath = studentPath, DateTime = DateTime.Now, StructuralIssues = result.StructuralIssues.Select(issue => new ValidationIssue() { Message = issue // TODO: add assignment url for HelpUrl }).ToList(), FunctionalIssues = result.FunctionalIssues.Select(issue => new ValidationIssue() { Message = issue }).ToList(), DesignIssues = result.DesignIssues.Select(issue => new ValidationIssue() { Message = issue.Message, HelpUrl = issue.HelpUrl, SourceUrl = BuildGitHubSourceUrl(ownerName, repoName, branchName, issue.Path, issue.Line) }).ToList(), StyleIssues = result.StyleIssues.Select(issue => new ValidationIssue() { Message = issue.Message, HelpUrl = issue.HelpUrl, SourceUrl = BuildGitHubSourceUrl(ownerName, repoName, branchName, issue.Path, issue.Line) }).ToList() }; _context.ValidationAttempts.Add(attempt); await _context.SaveChangesAsync(); } /// /// Extracts and saves the specification into a local folder /// and sets the path variable /// /// The assignment with the specification to save /// True on success, false otherwise public bool SaveAssignmentSpecification(Assignment assignment) { return SaveAssignmentSpecificationAsync(assignment).GetAwaiter().GetResult(); } /// /// Extracts and saves the specification into a local folder /// and sets the path variable /// /// The assignment with the specification to save /// True on success, false otherwise public async Task SaveAssignmentSpecificationAsync(Assignment assignment) { // Save the assignment specification try { var tmpPath = Path.GetTempFileName(); var randomName = Path.GetRandomFileName(); var specificationPath = Path.Combine(_rootPath, "specifications", randomName); using (var stream = System.IO.File.Create(tmpPath)) { await assignment.Specification.CopyToAsync(stream); } await Task.Run(() => ZipFile.ExtractToDirectory(tmpPath, specificationPath, true)); await Task.Run(() => System.IO.File.Delete(tmpPath)); // The unzipped file adds an extra directory - should be archive.FileName with the .zip dropped var dirName = Path.GetFileNameWithoutExtension(assignment.Specification.FileName); assignment.SpecificationPath = Path.Combine(specificationPath, dirName); } catch (Exception) { return false; } return true; } #endregion #region Helper Methods /// /// Helper method that: /// 1. Clones the repo (if it does not yet exist) and /// 2. Checks out the specified branch /// /// The user the repository belongs to /// The assignment which this repo is associated with /// The owner of the repo (typically the organization the GitHub assignment was created from) /// The name of the repository that is being cloned /// The name of the branch to check out /// The path to the cloned repo private string CloneAndCheckout(User user, Assignment assignment, string ownerName, string repoName, string branchName) { // git@github.com:ksu-cis/webhook-test-zombiepaladin.git var filePath = $"{_rootPath}\\submissions\\{assignment.ID}\\{user.ID}"; var repoUrl = $"git@github.com:{ownerName}/{repoName}.git"; // Clone the repo if it does not yet exist if (!Directory.Exists(filePath)) { Directory.CreateDirectory(filePath); Repository.Clone(repoUrl, filePath); } using var repo = new Repository(filePath); // Create a local branch to track the remote one if it does not yet exist if (repo.Branches[branchName] is null) { Branch trackedBranch = repo.Branches[$"refs/remotes/origin/{branchName}"]; Branch localBranch = repo.CreateBranch(branchName, trackedBranch.Tip); repo.Branches.Update(localBranch, b => b.TrackedBranch = trackedBranch.CanonicalName); } // Checkout the specified branch var branch = repo.Branches[branchName]; if (branch != null) Commands.Checkout(repo, branch); return filePath; } /// /// A helper method to build a URL pointing at the specified number /// in the file at in the branch found /// in the repo in the oranization . /// /// The organization that owns the repo /// The name of the repo /// The repo branch of interest /// The path to the file of interest /// The line number of that file to highlight /// private static string BuildGitHubSourceUrl(string ownerName, string repoName, string branchName, string path, string line) { if (path.Length == 0) return ""; // Build the souceURL var sb = new StringBuilder(); sb.Append("https://github.com/"); sb.Append(ownerName); sb.Append("/"); sb.Append(repoName); sb.Append("/blob/"); sb.Append(branchName); sb.Append(path); sb.Append("#L"); sb.Append(line); return sb.ToString(); } private static string BuildGitHubSourceUrl(string repoUrl, string commitIdentifier, string path, string line) { if (path.Length == 0) return ""; // Build the souceURL var sb = new StringBuilder(); sb.Append(repoUrl); sb.Append("/tree/"); sb.Append(commitIdentifier); sb.Append(path); sb.Append("#L"); sb.Append(line); return sb.ToString(); } #endregion } }