Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CloneDevOpsTemplate/Controllers/ProjectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ await Task.WhenAll(
_cloneManager.CloneRepositoriesAsync(templateProjectId, cloneProjectResult.Project.Id)
);

await _cloneManager.CloneGitPullRequestsAsync(templateProjectId, cloneProjectResult.Project.Id);

return RedirectToAction("Project", new { projectId = cloneProjectResult.Project.Id });
}

Expand Down
22 changes: 21 additions & 1 deletion CloneDevOpsTemplate/Controllers/RepositoryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task<IActionResult> PullRequests(Guid projectId)
return View(pullRequests.Value);
}

pullRequests = await _repositoryService.GetGitPullRequest(projectId) ?? new();
pullRequests = await _repositoryService.GetGitPullRequestAsync(projectId) ?? new();
return View(pullRequests.Value);
}

Expand All @@ -72,4 +72,24 @@ public async Task<IActionResult> CloneRepository(Guid templateProjectId, Guid pr

return await CloneRepository();
}

[HttpGet]
public async Task<IActionResult> ClonePullRequests()
{
return await CloneRepository();
}

[HttpPost]
public async Task<IActionResult> ClonePullRequests(Guid templateProjectId, Guid projectId)
{
if (!ModelState.IsValid)
{
return await ClonePullRequests();
}

await _cloneManager.CloneGitPullRequestsAsync(templateProjectId, projectId);
ViewBag.SuccessMessage = "Success";

return await ClonePullRequests();
}
}
3 changes: 2 additions & 1 deletion CloneDevOpsTemplate/IServices/IRepositoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public interface IRepositoryService
Task<HttpResponseMessage> DeleteRepositoryAsync(Guid projectId, Guid repositoryId);
Task<GitImportRequest?> CreateImportRequestAsync(Guid projectId, Guid repositoryId, string sourceRepositoryRemoteUrl, Guid serviceEndpointId);
Task<GitImportRequest?> GetImportRequestAsync(Guid projectId, Guid repositoryId, int importRequestId);
Task<GitPullRequests?> GetGitPullRequest(Guid projectId);
Task<GitPullRequests?> GetGitPullRequestAsync(Guid projectId);
Task<HttpResponseMessage> CreateGitPullRequestAsync(Guid projectId, Guid repositoryId, GitPullRequestBase pullRequest);
}
29 changes: 29 additions & 0 deletions CloneDevOpsTemplate/Managers/CloneManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,33 @@ public async Task CloneTeamIterationsAsync(Guid templateProjectId, Guid projectI
}
}
}

public async Task CloneGitPullRequestsAsync(Guid templateProjectId, Guid projectId)
{
var templatePullRequests = await _repositoryService.GetGitPullRequestAsync(templateProjectId) ?? new();
var mappedRespositories = await MapRepositories(templateProjectId, projectId);
foreach (var templatePullRequest in templatePullRequests.Value)
{
var repository = mappedRespositories.GetValueOrDefault(templatePullRequest.Repository.Id);
await _repositoryService.CreateGitPullRequestAsync(projectId, repository, templatePullRequest);
}
}

public async Task<Dictionary<Guid, Guid>> MapRepositories(Guid templateProjectId, Guid projectId)
{
var mappedRespositories = new Dictionary<Guid, Guid>();
var repositories = await _repositoryService.GetRepositoriesAsync(projectId) ?? new();
var templateRepositories = await _repositoryService.GetRepositoriesAsync(templateProjectId) ?? new();

foreach (var templateRepository in templateRepositories.Value)
{
var mappedRespository = repositories.Value.FirstOrDefault(r => r.Name == templateRepository.Name);
if (mappedRespository is not null)
{
mappedRespositories.Add(templateRepository.Id, mappedRespository.Id);
}
}

return mappedRespositories;
}
}
1 change: 1 addition & 0 deletions CloneDevOpsTemplate/Managers/ICloneManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ public interface ICloneManager
Task CloneTeamsAndSettingsAndBoardsAsync(Project templateProject, Project project);
Task CloneTeamSettingsAsync(Guid templateProjectId, Guid projectId, Guid templateTeamId, Guid projectTeamId);
Task CloneTeamIterationsAsync(Guid templateProjectId, Guid projectId, Guid templateTeamId, Guid projectTeamId);
Task CloneGitPullRequestsAsync(Guid templateProjectId, Guid projectId);
}
11 changes: 11 additions & 0 deletions CloneDevOpsTemplate/Models/Identity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CloneDevOpsTemplate.Models;

public class IdentityRef
{
public string DisplayName { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string Descriptor { get; set; } = string.Empty;
public string DirectoryAlias { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public bool IsDeletedInOrigin { get; set; }
}
20 changes: 12 additions & 8 deletions CloneDevOpsTemplate/Models/PullRequests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,29 @@ public class GitPullRequests
public GitPullRequest[] Value { get; set; } = [];
}

public class GitPullRequest
public class GitPullRequestBase
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string SourceRefName { get; set; } = string.Empty;
public string TargetRefName { get; set; } = string.Empty;
public IdentityRef CreatedBy { get; set; } = new();
public bool IsDraft { get; set; }
public IdentityRef[] Reviewers { get; set; } = [];
}

public class GitPullRequest : GitPullRequestBase
{
public Repository Repository { get; set; } = new();
public int PullRequestId { get; set; }
public int CodeReviewId { get; set; }
public PullRequestStatus Status { get; set; }
public User CreatedBy { get; set; } = new();
public DateTime CreationDate { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string SourceRefName { get; set; } = string.Empty;
public string TargetRefName { get; set; } = string.Empty;
public PullRequestAsyncStatus MergeStatus { get; set; }
public bool IsDraft { get; set; }
public Guid MergeId { get; set; }
public GitCommitRef LastMergeSourceCommit { get; set; } = new();
public GitCommitRef LastMergeTargetCommit { get; set; } = new();
public GitCommitRef LastMergeCommit { get; set; } = new();
public User[] Reviewers { get; set; } = [];
public bool SupportsIterations { get; set; }
}

Expand Down
8 changes: 1 addition & 7 deletions CloneDevOpsTemplate/Models/Services.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,13 @@ public class ServiceModelBase
public class ServiceModel : ServiceModelBase
{
public Guid Id { get; set; }
public User CreatedBy { get; set; } = new();
public IdentityRef CreatedBy { get; set; } = new();
public string Description { get; set; } = string.Empty;
public bool IsShared { get; set; }
public bool IsOutdated { get; set; }
public DateTime CreationDate { get; set; }
}

public class User
{
public string DisplayName { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}

public class ServiceAuthorization
{
public string Scheme { get; set; } = string.Empty;
Expand Down
7 changes: 6 additions & 1 deletion CloneDevOpsTemplate/Services/RepositoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ public Task<HttpResponseMessage> DeleteRepositoryAsync(Guid projectId, Guid repo
return _client.GetFromJsonAsync<GitImportRequest>($"{projectId}/_apis/git/repositories/{repositoryId}/importRequests/{importRequestId}");
}

public Task<GitPullRequests?> GetGitPullRequest(Guid projectId)
public Task<GitPullRequests?> GetGitPullRequestAsync(Guid projectId)
{
return _client.GetFromJsonAsync<GitPullRequests>($"{projectId}/_apis/git/pullrequests");
}

public Task<HttpResponseMessage> CreateGitPullRequestAsync(Guid projectId, Guid repositoryId, GitPullRequestBase pullRequest)
{
return _client.PostAsJsonAsync($"{projectId}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1", pullRequest);
}
}
10 changes: 10 additions & 0 deletions CloneDevOpsTemplate/Views/Repository/ClonePullRequests.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@model ProjectBase[]
@{
ViewData["Title"] = "Clone pull requests";
}

<h1>Clone pull requests</h1>

@{
await Html.RenderPartialAsync("_CloneByProject", Tuple.Create(Model, "Repository", "ClonePullRequests"));
}
4 changes: 4 additions & 0 deletions CloneDevOpsTemplate/Views/Shared/_Header.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
<a class="dropdown-item" asp-controller="Repository"
asp-action="CloneRepository">Repository</a>
</li>
<li>
<a class="dropdown-item" asp-controller="Repository" asp-action="ClonePullRequests">Pull
requests</a>
</li>
</ul>
</li>
<li class="nav-item-button">
Expand Down
78 changes: 75 additions & 3 deletions CloneDevOpsTemplateTest/Controllers/RepositoryControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ public async Task PullRequests_InvalidModelState_ReturnsDefaultView()
{
// Arrange
_controller.ModelState.AddModelError("Error", "Invalid model state");
var mockPullRequests = new GitPullRequests { Value = Array.Empty<GitPullRequest>() };

// Act
var result = await _controller.PullRequests(Guid.NewGuid());
Expand All @@ -219,7 +218,7 @@ public async Task PullRequests_ReturnsViewWithPullRequests()
var projectId = Guid.NewGuid();
var mockPullRequests = new GitPullRequests { Value = Array.Empty<GitPullRequest>() };
_mockRepositoryService
.Setup(service => service.GetGitPullRequest(projectId))
.Setup(service => service.GetGitPullRequestAsync(projectId))
.ReturnsAsync(mockPullRequests);

// Act
Expand All @@ -236,7 +235,7 @@ public async Task PullRequests_ReturnsViewWithEmptyPullRequests_WhenServiceRetur
// Arrange
var projectId = Guid.NewGuid();
_mockRepositoryService
.Setup(service => service.GetGitPullRequest(projectId))
.Setup(service => service.GetGitPullRequestAsync(projectId))
.ReturnsAsync((GitPullRequests?)null);

// Act
Expand All @@ -247,4 +246,77 @@ public async Task PullRequests_ReturnsViewWithEmptyPullRequests_WhenServiceRetur
var viewModel = Assert.IsType<GitPullRequest[]>(viewResult.Model);
Assert.Empty(viewModel);
}

[Fact]
public async Task ClonePullRequests_Get_ReturnsViewWithProjects()
{
// Arrange
var mockProjects = new Projects { Value = Array.Empty<Project>() };
_mockProjectService
.Setup(service => service.GetAllProjectsAsync())
.ReturnsAsync(mockProjects);

// Act
var result = await _controller.ClonePullRequests();

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Equal(mockProjects.Value, viewResult.Model);
}

[Fact]
public async Task ClonePullRequests_Get_ReturnsViewWithEmptyProjects_WhenServiceReturnsNull()
{
// Arrange
_mockProjectService
.Setup(service => service.GetAllProjectsAsync())
.ReturnsAsync((Projects?)null);

// Act
var result = await _controller.ClonePullRequests();

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var viewModel = Assert.IsType<ProjectBase[]>(viewResult.Model);
Assert.Empty(viewModel);
}

[Fact]
public async Task ClonePullRequests_Post_InvalidModelState_ReturnsViewWithProjects()
{
// Arrange
_controller.ModelState.AddModelError("Error", "Invalid model state");
var mockProjects = new Projects { Value = Array.Empty<Project>() };
_mockProjectService
.Setup(service => service.GetAllProjectsAsync())
.ReturnsAsync(mockProjects);

// Act
var result = await _controller.ClonePullRequests(Guid.NewGuid(), Guid.NewGuid());

// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Equal(mockProjects.Value, viewResult.Model);
}

[Fact]
public async Task ClonePullRequests_Post_ValidModelState_ClonesPullRequestsAndReturnsViewWithProjects()
{
// Arrange
var templateProjectId = Guid.NewGuid();
var projectId = Guid.NewGuid();
var mockProjects = new Projects { Value = Array.Empty<Project>() };
_mockProjectService
.Setup(service => service.GetAllProjectsAsync())
.ReturnsAsync(mockProjects);

// Act
var result = await _controller.ClonePullRequests(templateProjectId, projectId);

// Assert
_mockCloneManager.Verify(manager => manager.CloneGitPullRequestsAsync(templateProjectId, projectId), Times.Once);
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Equal(mockProjects.Value, viewResult.Model);
Assert.Equal("Success", _controller.ViewBag.SuccessMessage);
}
}
39 changes: 39 additions & 0 deletions CloneDevOpsTemplateTest/Managers/CloneManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,43 @@ public async Task CloneClassificationNodes_ShouldCloneNodesCorrectly()
_mockIterationService.Verify(s => s.GetAllAsync(templateProjectId, structureGroup), Times.Once);
_mockIterationService.Verify(s => s.CreateAsync(projectId, templateClassificationNodes, structureGroup, ""), Times.Once);
}

[Fact]
public async Task CloneGitPullRequestsAsync_ShouldClonePullRequests()
{
// Arrange
var templateProjectId = Guid.NewGuid();
var projectId = Guid.NewGuid();

var templateRepositoryId = Guid.NewGuid();
var mappedRepositoryId = Guid.NewGuid();

var templatePullRequests = new GitPullRequests
{
Value =
[
new GitPullRequest
{
Repository = new Repository { Id = templateRepositoryId },
Title = "Sample Pull Request"
}
]
};

_mockRepositoryService.Setup(s => s.GetGitPullRequestAsync(templateProjectId))
.ReturnsAsync(templatePullRequests);

_mockRepositoryService.Setup(s => s.GetRepositoriesAsync(templateProjectId))
.ReturnsAsync(new Repositories { Value = [new Repository { Name = "Repo1", Id = templateRepositoryId }] });

_mockRepositoryService.Setup(s => s.GetRepositoriesAsync(projectId))
.ReturnsAsync(new Repositories { Value = [new Repository { Name = "Repo1", Id = mappedRepositoryId }] });

// Act
await _cloneManager.CloneGitPullRequestsAsync(templateProjectId, projectId);

// Assert
_mockRepositoryService.Verify(s => s.GetGitPullRequestAsync(templateProjectId), Times.Once);
_mockRepositoryService.Verify(s => s.CreateGitPullRequestAsync(projectId, mappedRepositoryId, It.IsAny<GitPullRequest>()), Times.Once);
}
}
Loading