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
}
}