From 71a1d1a5551c1611792fa5af6fb183f880e90a8e Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 16 Sep 2024 18:02:17 +0200 Subject: [PATCH 01/20] add auxiliary repository view --- ...ogrammingExerciseParticipationService.java | 17 ++ .../service/ProgrammingExerciseService.java | 11 + ...grammingExerciseParticipationResource.java | 22 +- .../web/ProgrammingExerciseResource.java | 20 ++ .../AuxiliaryRepositoryResource.java | 223 ++++++++++++++++++ ...y-repository-buttons-detail.component.html | 9 +- .../detail-overview-list.component.html | 2 +- ...ming-exercise-management-routing.module.ts | 24 ++ ...gramming-exercise-participation.service.ts | 10 + .../services/programming-exercise.service.ts | 11 + .../code-editor/model/code-editor.model.ts | 5 +- .../code-editor-conflict-state.service.ts | 3 + ...ditor-domain-dependent-endpoint.service.ts | 2 + .../service/code-editor-domain.service.ts | 9 +- .../commit-history.component.ts | 63 ++++- .../repository-view.component.ts | 30 ++- 16 files changed, 438 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseParticipationService.java index 4731295ed3c3..fd5d93cc660c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseParticipationService.java @@ -31,6 +31,7 @@ import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository; import de.tum.cit.aet.artemis.exercise.repository.TeamRepository; +import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -517,6 +518,22 @@ public List getCommitInfos(ProgrammingExerciseParticipation parti } } + /** + * Get the commits information for the given auxiliary repository. + * + * @param auxiliaryRepository the auxiliary repository for which to get the commits. + * @return a list of CommitInfo DTOs containing author, timestamp, commit-hash and commit message. + */ + public List getAuxiliaryRepositoryCommitInfos(AuxiliaryRepository auxiliaryRepository) { + try { + return gitService.getCommitInfos(auxiliaryRepository.getVcsRepositoryUri()); + } + catch (GitAPIException e) { + log.error("Could not get commit infos for auxiliaryRepository {} with repository uri {}", auxiliaryRepository.getId(), auxiliaryRepository.getVcsRepositoryUri()); + return List.of(); + } + } + /** * Get the commits information for the test repository of the given participation's exercise. * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index 00e854f1d218..bdc88014e363 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1009,4 +1009,15 @@ public ProgrammingExercise loadProgrammingExercise(long exerciseId, boolean with programmingExerciseTaskService.replaceTestIdsWithNames(programmingExercise); return programmingExercise; } + + /** + * Load a programming exercise, only with eager auxiliary repositories + * + * @param exerciseId the ID of the programming exercise to load + * @return the loaded programming exercise entity + */ + public ProgrammingExercise loadProgrammingExerciseWithAuxiliaryRepositories(long exerciseId) { + final Set fetchOptions = Set.of(AuxiliaryRepositories); + return programmingExerciseRepository.findByIdWithDynamicFetchElseThrow(exerciseId, fetchOptions); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index a1e104e6fde4..a962af1bb411 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -44,6 +44,7 @@ import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.dto.CommitInfoDTO; +import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseParticipationService; @@ -79,11 +80,13 @@ public class ProgrammingExerciseParticipationResource { private final StudentExamRepository studentExamRepository; + private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + public ProgrammingExerciseParticipationResource(ProgrammingExerciseParticipationService programmingExerciseParticipationService, ResultRepository resultRepository, ParticipationRepository participationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingSubmissionService submissionService, ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService, ResultService resultService, ParticipationAuthorizationCheckService participationAuthCheckService, RepositoryService repositoryService, - StudentExamRepository studentExamRepository) { + StudentExamRepository studentExamRepository, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { this.programmingExerciseParticipationService = programmingExerciseParticipationService; this.participationRepository = participationRepository; this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; @@ -95,6 +98,7 @@ public ProgrammingExerciseParticipationResource(ProgrammingExerciseParticipation this.participationAuthCheckService = participationAuthCheckService; this.repositoryService = repositoryService; this.studentExamRepository = studentExamRepository; + this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; } /** @@ -312,17 +316,20 @@ public ResponseEntity> getCommitHistoryForParticipationRepo( * * @param exerciseID the id of the exercise for which to retrieve the commit history * @param repositoryType the type of the repository for which to retrieve the commit history + * @param repositoryId the id of the repository * @return the ResponseEntity with status 200 (OK) and with body a list of commitInfo DTOs with the commits information of the repository */ @GetMapping("programming-exercise/{exerciseID}/commit-history/{repositoryType}") @EnforceAtLeastTutor - public ResponseEntity> getCommitHistoryForTemplateSolutionOrTestRepo(@PathVariable long exerciseID, @PathVariable RepositoryType repositoryType) { + public ResponseEntity> getCommitHistoryForTemplateSolutionOrTestRepo(@PathVariable long exerciseID, @PathVariable RepositoryType repositoryType, + @RequestParam long repositoryId) { boolean isTemplateRepository = repositoryType.equals(RepositoryType.TEMPLATE); boolean isSolutionRepository = repositoryType.equals(RepositoryType.SOLUTION); boolean isTestRepository = repositoryType.equals(RepositoryType.TESTS); + boolean isAuxiliaryRepository = repositoryType.equals(RepositoryType.AUXILIARY); ProgrammingExerciseParticipation participation; - if (!isTemplateRepository && !isSolutionRepository && !isTestRepository) { + if (!isTemplateRepository && !isSolutionRepository && !isTestRepository && !isAuxiliaryRepository) { throw new BadRequestAlertException("Invalid repository type", ENTITY_NAME, "invalidRepositoryType"); } else if (isTemplateRepository) { @@ -332,6 +339,15 @@ else if (isTemplateRepository) { participation = programmingExerciseParticipationService.findSolutionParticipationByProgrammingExerciseId(exerciseID); } participationAuthCheckService.checkCanAccessParticipationElseThrow(participation); + + if (isAuxiliaryRepository) { + var auxiliaryRepo = auxiliaryRepositoryRepository.findByIdElseThrow(repositoryId); + if (!auxiliaryRepo.getExercise().getId().equals(exerciseID)) { + throw new BadRequestAlertException("Invalid repository id", ENTITY_NAME, "invalidRepositoryId"); + } + return ResponseEntity.ok(programmingExerciseParticipationService.getAuxiliaryRepositoryCommitInfos(auxiliaryRepo)); + } + if (isTestRepository) { return ResponseEntity.ok(programmingExerciseParticipationService.getCommitInfosTestRepo(participation)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 7462f19512c1..9a29ed231896 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -531,6 +531,26 @@ public ResponseEntity getProgrammingExerciseWithTemplateAnd return ResponseEntity.ok(programmingExercise); } + /** + * GET /programming-exercises/:exerciseId/with-auxiliary-repository/:auxiliaryRepositoryId + * + * @param exerciseId the id of the programmingExercise to retrieve + * @param auxiliaryRepositoryId the id of the auxiliary repository of the exercise + * @return the ResponseEntity with status 200 (OK) and the programming exercise with template and solution participation, or with status 404 (Not Found) + */ + @GetMapping("programming-exercises/{exerciseId}/with-auxiliary-repository/{auxiliaryRepositoryId}") + @EnforceAtLeastTutorInExercise + public ResponseEntity getProgrammingExerciseWithTemplateAndSolutionParticipation(@PathVariable long exerciseId, @PathVariable long auxiliaryRepositoryId) { + + log.debug("REST request to get programming exercise with template and solution participation : {}", exerciseId); + final var programmingExercise = programmingExerciseService.loadProgrammingExerciseWithAuxiliaryRepositories(exerciseId); + if (programmingExercise.getAuxiliaryRepositories().stream().noneMatch(id -> id.getId() == auxiliaryRepositoryId)) { + throw new BadRequestAlertException("The auxiliary repository Id does not belong to the exercise.", "Exercise", + ProgrammingExerciseResourceErrorKeys.INVALID_AUXILIARY_REPOSITORY_ID); + } + return ResponseEntity.ok(programmingExercise); + } + /** * DELETE /programming-exercises/:id : delete the "id" programmingExercise. * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java new file mode 100644 index 000000000000..ba3acb13be4a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -0,0 +1,223 @@ +package de.tum.cit.aet.artemis.programming.web.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import jakarta.servlet.http.HttpServletRequest; + +import org.eclipse.jgit.api.errors.CheckoutConflictException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; +import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.service.ProfileService; +import de.tum.cit.aet.artemis.core.service.feature.Feature; +import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle; +import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; +import de.tum.cit.aet.artemis.programming.domain.FileType; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.domain.Repository; +import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; +import de.tum.cit.aet.artemis.programming.dto.FileMove; +import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; +import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; +import de.tum.cit.aet.artemis.programming.service.GitService; +import de.tum.cit.aet.artemis.programming.service.RepositoryAccessService; +import de.tum.cit.aet.artemis.programming.service.RepositoryService; +import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; +import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; + +/** + * Executes requested actions on the test repository of a programming exercise. Only available to TAs, Instructors and Admins. + */ +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class AuxiliaryRepositoryResource extends RepositoryResource { + + private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + + public AuxiliaryRepositoryResource(ProfileService profileService, UserRepository userRepository, AuthorizationCheckService authCheckService, GitService gitService, + RepositoryService repositoryService, Optional versionControlService, ProgrammingExerciseRepository programmingExerciseRepository, + RepositoryAccessService repositoryAccessService, Optional localVCServletService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { + super(profileService, userRepository, authCheckService, gitService, repositoryService, versionControlService, programmingExerciseRepository, repositoryAccessService, + localVCServletService); + this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; + } + + @Override + Repository getRepository(Long auxiliaryRepositoryId, RepositoryActionType repositoryActionType, boolean pullOnGet) throws GitAPIException { + final var auxiliaryRepository = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); + User user = userRepository.getUserWithGroupsAndAuthorities(); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, auxiliaryRepository.getExercise(), user, "test"); + final var repoUri = auxiliaryRepository.getVcsRepositoryUri(); + return gitService.getOrCheckoutRepository(repoUri, pullOnGet); + } + + @Override + VcsRepositoryUri getRepositoryUri(Long auxiliaryRepositoryId) { + var auxRepo = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); + return auxRepo.getVcsRepositoryUri(); + } + + @Override + boolean canAccessRepository(Long auxiliaryRepositoryId) { + try { + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId).getExercise(), + userRepository.getUserWithGroupsAndAuthorities(), "auxiliary"); + } + catch (AccessForbiddenException e) { + return false; + } + return true; + } + + @Override + String getOrRetrieveBranchOfDomainObject(Long auxiliaryRepositoryId) { + AuxiliaryRepository auxiliaryRep = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); + // auxiliaryRep + + // ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); + // return versionControlService.orElseThrow().getOrRetrieveBranchOfExercise() + return "main"; + } + + @Override + @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/files", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + public ResponseEntity> getFiles(@PathVariable Long auxiliaryRepositoryId) { + return super.getFiles(auxiliaryRepositoryId); + } + + @Override + @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @EnforceAtLeastTutor + public ResponseEntity getFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filename) { + return super.getFile(auxiliaryRepositoryId, filename); + } + + @Override + @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity createFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filePath, HttpServletRequest request) { + return super.createFile(auxiliaryRepositoryId, filePath, request); + } + + @Override + @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity createFolder(@PathVariable Long auxiliaryRepositoryId, @RequestParam("folder") String folderPath, HttpServletRequest request) { + return super.createFolder(auxiliaryRepositoryId, folderPath, request); + } + + @Override + @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity renameFile(@PathVariable Long auxiliaryRepositoryId, @RequestBody FileMove fileMove) { + return super.renameFile(auxiliaryRepositoryId, fileMove); + } + + @Override + @DeleteMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity deleteFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filename) { + return super.deleteFile(auxiliaryRepositoryId, filename); + } + + @Override + @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + public ResponseEntity pullChanges(@PathVariable Long auxiliaryRepositoryId) { + return super.pullChanges(auxiliaryRepositoryId); + } + + @Override + @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity commitChanges(@PathVariable Long auxiliaryRepositoryId) { + return super.commitChanges(auxiliaryRepositoryId); + } + + @Override + @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity resetToLastCommit(@PathVariable Long auxiliaryRepositoryId) { + return super.resetToLastCommit(auxiliaryRepositoryId); + } + + @Override + @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceAtLeastTutor + public ResponseEntity getStatus(@PathVariable Long auxiliaryRepositoryId) throws GitAPIException { + return super.getStatus(auxiliaryRepositoryId); + } + + /** + * Update a list of files in a test repository based on the submission's content. + * + * @param auxiliaryRepositoryId of exercise to which the files belong + * @param submissions information about the file updates + * @param commit whether to commit after updating the files + * @param principal used to check if the user can update the files + * @return {Map} file submissions or the appropriate http error + */ + @PutMapping("auxiliary-repository/{auxiliaryRepositoryId}/files") + @EnforceAtLeastTutor + public ResponseEntity> updateTestFiles(@PathVariable("auxiliaryRepositoryId") Long auxiliaryRepositoryId, @RequestBody List submissions, + @RequestParam Boolean commit, Principal principal) { + + if (versionControlService.isEmpty()) { + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "VCSNotPresent"); + } + + ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(auxiliaryRepositoryId); + + Repository repository; + try { + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(true, exercise, userRepository.getUserWithGroupsAndAuthorities(principal.getName()), "test"); + repository = gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true); + } + catch (AccessForbiddenException e) { + FileSubmissionError error = new FileSubmissionError(auxiliaryRepositoryId, "noPermissions"); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, error.getMessage(), error); + } + catch (CheckoutConflictException | WrongRepositoryStateException ex) { + FileSubmissionError error = new FileSubmissionError(auxiliaryRepositoryId, "checkoutConflict"); + throw new ResponseStatusException(HttpStatus.CONFLICT, error.getMessage(), error); + } + catch (GitAPIException ex) { + FileSubmissionError error = new FileSubmissionError(auxiliaryRepositoryId, "checkoutFailed"); + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, error.getMessage(), error); + } + return saveFilesAndCommitChanges(auxiliaryRepositoryId, submissions, commit, repository); + } +} diff --git a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html index c22ec3d50039..374b8d3a9c5b 100644 --- a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html +++ b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html @@ -1,9 +1,14 @@
    - @for (auxiliaryRepository of detail.data.auxiliaryRepositories; track auxiliaryRepository) { + @for (auxiliaryRepository of detail.data.auxiliaryRepositories; track auxiliaryRepository.id) { @if (auxiliaryRepository.id && auxiliaryRepository.repositoryUri && detail.data.exerciseId) {
  • Repository: {{ auxiliaryRepository.name }} - + {{ section.headline | artemisTranslate }}
    - @for (detail of section.details; track detail) { + @for (detail of section.details; track $index) { @if (!!detail) { @if (detail.title) {
    diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts index 54c277c9bd7d..8b87e1ee720d 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts @@ -171,6 +171,18 @@ export const routes: Routes = [ }, canActivate: [UserRouteAccessService, LocalVCGuard], }, + { + path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/:auxiliaryRepositoryId', + component: RepositoryViewComponent, + data: { + authorities: [Authority.ADMIN, Authority.INSTRUCTOR, Authority.EDITOR, Authority.TA], + pageTitle: 'artemisApp.repository.title', + flushRepositoryCacheAfter: 900000, // 15 min + participationCache: {}, + repositoryCache: {}, + }, + canActivate: [UserRouteAccessService, LocalVCGuard], + }, { path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/commit-history', component: CommitHistoryComponent, @@ -183,6 +195,18 @@ export const routes: Routes = [ }, canActivate: [LocalVCGuard], }, + { + path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/:repositoryId/commit-history', + component: CommitHistoryComponent, + data: { + authorities: [Authority.ADMIN, Authority.INSTRUCTOR, Authority.EDITOR], + pageTitle: 'artemisApp.repository.title', + flushRepositoryCacheAfter: 900000, // 15 min + participationCache: {}, + repositoryCache: {}, + }, + canActivate: [LocalVCGuard], + }, { path: ':courseId/programming-exercises/:exerciseId/repository/:repositoryType/commit-history/:commitHash', component: CommitDetailsViewComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index e5ece788a0fb..923ad47b7b5f 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -155,6 +155,16 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer } retrieveCommitHistoryForTemplateSolutionOrTests(exerciseId: number, repositoryType: string): Observable { + console.log(repositoryType); return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`); } + + retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, repositoryType: string, auxiliaryRepositoryId: number): Observable { + console.log(repositoryType); + const params: { [key: string]: number | string } = {}; + if (repositoryType) { + params['repositoryId'] = auxiliaryRepositoryId; + } + return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`, { params: params }); + } } diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts index 82077ff4ffd6..c20975d6e2bc 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts @@ -283,6 +283,17 @@ export class ProgrammingExerciseService { ); } + /** + * Finds the programming exercise for the given exerciseId with its auxiliary repositories + * @param programmingExerciseId of the programming exercise to retrieve + * @param auxiliaryRepositoryId of the auxiliary repository + */ + findWithAuxiliaryRepository(programmingExerciseId: number, auxiliaryRepositoryId: number): Observable { + return this.http.get(`${this.resourceUrl}/${programmingExerciseId}/with-auxiliary-repository/${auxiliaryRepositoryId}`, { + observe: 'response', + }); + } + private setLatestResultForTemplateAndSolution(programmingExercise: ProgrammingExercise) { if (programmingExercise.templateParticipation) { const latestTemplateResult = this.getLatestResult(programmingExercise.templateParticipation); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/model/code-editor.model.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/model/code-editor.model.ts index a3215aa04c48..555b5d0e84d8 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/model/code-editor.model.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/model/code-editor.model.ts @@ -2,6 +2,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; +import { AuxiliaryRepository } from 'app/entities/programming/programming-exercise-auxiliary-repository-model'; /** * Enumeration defining type of the exported file. @@ -49,6 +50,7 @@ export type FileSubmission = { [fileName: string]: string | undefined }; export enum DomainType { PARTICIPATION = 'PARTICIPATION', TEST_REPOSITORY = 'TEST_REPOSITORY', + AUXILIARY_REPOSITORY = 'AUXILIARY_REPOSITORY', } /** @@ -94,7 +96,8 @@ export enum ResizeType { export type DomainParticipationChange = [DomainType.PARTICIPATION, StudentParticipation | TemplateProgrammingExerciseParticipation | SolutionProgrammingExerciseParticipation]; export type DomainTestRepositoryChange = [DomainType.TEST_REPOSITORY, ProgrammingExercise]; -export type DomainChange = DomainParticipationChange | DomainTestRepositoryChange; +export type DomainAuxiliaryRepositoryChange = [DomainType.AUXILIARY_REPOSITORY, AuxiliaryRepository]; +export type DomainChange = DomainParticipationChange | DomainTestRepositoryChange | DomainAuxiliaryRepositoryChange; /** * Enumeration defining the state of git. diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts index eba9259107c1..a045a3b2b62b 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service.ts @@ -66,6 +66,9 @@ export class CodeEditorConflictStateService extends DomainDependentService imple private getDomainKey = () => { const [domainType, domainValue] = this.domain; + if (domainType === DomainType.AUXILIARY_REPOSITORY) { + return `auxiliary-${domainValue.id!.toString()}`; + } return `${domainType === DomainType.PARTICIPATION ? 'participation' : 'test'}-${domainValue.id!.toString()}`; }; } diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts index fe6e71ac586a..b124042fec13 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain-dependent-endpoint.service.ts @@ -35,6 +35,8 @@ export abstract class DomainDependentEndpointService extends DomainDependentServ return `api/repository/${domainValue.id}`; case DomainType.TEST_REPOSITORY: return `api/test-repository/${domainValue.id}`; + case DomainType.AUXILIARY_REPOSITORY: + return `api/auxiliary-repository/${domainValue.id}`; } } } diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain.service.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain.service.ts index 84d080b67efb..cebaae778af9 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/service/code-editor-domain.service.ts @@ -1,6 +1,11 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; -import { DomainChange, DomainParticipationChange, DomainTestRepositoryChange } from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; +import { + DomainAuxiliaryRepositoryChange, + DomainChange, + DomainParticipationChange, + DomainTestRepositoryChange, +} from 'app/exercises/programming/shared/code-editor/model/code-editor.model'; /** * This service provides subscribing services with the most recently selected domain (participation vs repository). @@ -9,7 +14,7 @@ import { DomainChange, DomainParticipationChange, DomainTestRepositoryChange } f @Injectable({ providedIn: 'root' }) export class DomainService { protected domain: DomainChange; - private subject = new BehaviorSubject(undefined); + private subject = new BehaviorSubject(undefined); constructor() {} diff --git a/src/main/webapp/app/localvc/commit-history/commit-history.component.ts b/src/main/webapp/app/localvc/commit-history/commit-history.component.ts index fe98dd02cb53..22b513cb6530 100644 --- a/src/main/webapp/app/localvc/commit-history/commit-history.component.ts +++ b/src/main/webapp/app/localvc/commit-history/commit-history.component.ts @@ -24,6 +24,7 @@ export class CommitHistoryComponent implements OnInit, OnDestroy { participationId: number; exerciseId: number; repositoryType: string; + repositoryId?: number; paramSub: Subscription; commits: CommitInfo[]; commitsInfoSubscription: Subscription; @@ -53,6 +54,7 @@ export class CommitHistoryComponent implements OnInit, OnDestroy { this.participationId = Number(params['participationId']); this.exerciseId = Number(params['exerciseId']); this.repositoryType = params['repositoryType']; + this.repositoryId = Number(params['repositoryId']); if (this.repositoryType) { this.loadDifferentParticipation(); } else { @@ -88,6 +90,8 @@ export class CommitHistoryComponent implements OnInit, OnDestroy { } else if (this.repositoryType === 'TESTS') { this.isTestRepository = true; this.participation = this.exercise.templateParticipation!; + } else if (this.repositoryType === 'AUXILIARY') { + this.participation = this.exercise.templateParticipation!; } }), ) @@ -121,26 +125,59 @@ export class CommitHistoryComponent implements OnInit, OnDestroy { } /** - * Retrieves the commit history for the participation and filters out the commits that have no submission. + * Retrieves the commit history and handles it depending on repository type * The last commit is always the template commit and is added to the list of commits. * @private */ private handleCommits() { - if (this.repositoryType) { - this.commitsInfoSubscription = this.programmingExerciseParticipationService - .retrieveCommitHistoryForTemplateSolutionOrTests(this.exerciseId, this.repositoryType) - .subscribe((commits) => { - this.commits = this.sortCommitsByTimestampDesc(commits); - if (!this.isTestRepository) { - this.setCommitResults(); - } - }); + if (!this.repositoryType) { + this.handleParticipationCommits(); + } else if (this.repositoryType === 'AUXILIARY') { + this.handleAuxiliaryRepositoryCommits(); } else { - this.commitsInfoSubscription = this.programmingExerciseParticipationService.retrieveCommitHistoryForParticipation(this.participation.id!).subscribe((commits) => { + this.handleTemplateSolutionTestRepositoryCommits(); + } + } + + /** + * Retrieves the commit history and filters out the commits that have no submission. + * The last commit is always the template commit and is added to the list of commits. + * @private + */ + private handleParticipationCommits() { + this.commitsInfoSubscription = this.programmingExerciseParticipationService.retrieveCommitHistoryForParticipation(this.participation.id!).subscribe((commits) => { + this.commits = this.sortCommitsByTimestampDesc(commits); + this.setCommitResults(); + }); + } + + /** + * Retrieves the commit history for an auxiliary repository + * The last commit is always the template commit and is added to the list of commits. + * @private + */ + private handleAuxiliaryRepositoryCommits() { + this.commitsInfoSubscription = this.programmingExerciseParticipationService + .retrieveCommitHistoryForAuxiliaryRepository(this.exerciseId, this.repositoryType, this.repositoryId!) + .subscribe((commits) => { this.commits = this.sortCommitsByTimestampDesc(commits); - this.setCommitResults(); }); - } + } + + /** + * Retrieves the commit history for template/solution/test repositories. + * The last commit is always the template commit and is added to the list of commits. + * @private + */ + private handleTemplateSolutionTestRepositoryCommits() { + this.commitsInfoSubscription = this.programmingExerciseParticipationService + .retrieveCommitHistoryForTemplateSolutionOrTests(this.exerciseId, this.repositoryType) + .subscribe((commits) => { + this.commits = this.sortCommitsByTimestampDesc(commits); + if (!this.isTestRepository) { + this.setCommitResults(); + } + }); } /** diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts index 22ee9747edbb..75502dcccd1f 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts @@ -42,7 +42,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { routeCommitHistory: string; repositoryUri: string; repositoryType: ProgrammingExerciseInstructorRepositoryType | 'USER'; - + auxiliaryRepositoryId: number; result: Result; resultHasInlineFeedback = false; showInlineFeedback = false; @@ -85,9 +85,13 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { this.participationCouldNotBeFetched = false; const exerciseId = Number(params['exerciseId']); const participationId = Number(params['participationId']); + const auxiliaryRepositoryId = Number(params['auxiliaryRepositoryId']); + console.log(auxiliaryRepositoryId); this.repositoryType = participationId ? 'USER' : params['repositoryType']; if (this.repositoryType === 'USER') { this.loadStudentParticipation(participationId); + } else if (this.repositoryType === 'AUXILIARY') { + this.loadAuxiliaryRepository(exerciseId, auxiliaryRepositoryId); } else { this.loadDifferentParticipation(this.repositoryType, exerciseId); } @@ -177,4 +181,28 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { }), ); } + + private loadAuxiliaryRepository(exerciseId: number, auxiliaryRepositoryId: number) { + this.programmingExerciseService + .findWithAuxiliaryRepository(exerciseId, auxiliaryRepositoryId) + .pipe( + tap((exerciseResponse) => { + this.exercise = exerciseResponse.body!; + const auxiliaryRepo = this.exercise.auxiliaryRepositories?.find((repo) => repo.id === auxiliaryRepositoryId); + if (auxiliaryRepo) { + this.domainService.setDomain([DomainType.AUXILIARY_REPOSITORY, auxiliaryRepo]); + this.repositoryUri = auxiliaryRepo.repositoryUri!; + } + }), + ) + .subscribe({ + next: () => { + this.loadingParticipation = false; + }, + error: () => { + this.participationCouldNotBeFetched = true; + this.loadingParticipation = false; + }, + }); + } } From bf991d37262d26079b131c699f1473f729843518 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 16 Sep 2024 20:31:23 +0200 Subject: [PATCH 02/20] get correct default branch --- .../web/repository/AuxiliaryRepositoryResource.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index ba3acb13be4a..40b0352a33ac 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -47,6 +47,7 @@ import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.RepositoryAccessService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; +import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCService; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; @@ -60,12 +61,16 @@ public class AuxiliaryRepositoryResource extends RepositoryResource { private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + private final LocalVCService localVcService; + public AuxiliaryRepositoryResource(ProfileService profileService, UserRepository userRepository, AuthorizationCheckService authCheckService, GitService gitService, RepositoryService repositoryService, Optional versionControlService, ProgrammingExerciseRepository programmingExerciseRepository, - RepositoryAccessService repositoryAccessService, Optional localVCServletService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { + RepositoryAccessService repositoryAccessService, Optional localVCServletService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, + LocalVCService localVcService) { super(profileService, userRepository, authCheckService, gitService, repositoryService, versionControlService, programmingExerciseRepository, repositoryAccessService, localVCServletService); this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; + this.localVcService = localVcService; } @Override @@ -98,11 +103,7 @@ boolean canAccessRepository(Long auxiliaryRepositoryId) { @Override String getOrRetrieveBranchOfDomainObject(Long auxiliaryRepositoryId) { AuxiliaryRepository auxiliaryRep = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); - // auxiliaryRep - - // ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - // return versionControlService.orElseThrow().getOrRetrieveBranchOfExercise() - return "main"; + return localVcService.getDefaultBranchOfRepository(auxiliaryRep.getVcsRepositoryUri()); } @Override From 5b83becec0c4e5e9e839293d7e74df4b6158d9ab Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 16 Sep 2024 21:58:20 +0200 Subject: [PATCH 03/20] remove console logs --- .../services/programming-exercise-participation.service.ts | 6 +----- .../localvc/repository-view/repository-view.component.ts | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index 923ad47b7b5f..1ee26446d35c 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -155,16 +155,12 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer } retrieveCommitHistoryForTemplateSolutionOrTests(exerciseId: number, repositoryType: string): Observable { - console.log(repositoryType); return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`); } retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, repositoryType: string, auxiliaryRepositoryId: number): Observable { - console.log(repositoryType); const params: { [key: string]: number | string } = {}; - if (repositoryType) { - params['repositoryId'] = auxiliaryRepositoryId; - } + params['repositoryId'] = auxiliaryRepositoryId; return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`, { params: params }); } } diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts index 75502dcccd1f..ca22978822b6 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts @@ -86,7 +86,6 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { const exerciseId = Number(params['exerciseId']); const participationId = Number(params['participationId']); const auxiliaryRepositoryId = Number(params['auxiliaryRepositoryId']); - console.log(auxiliaryRepositoryId); this.repositoryType = participationId ? 'USER' : params['repositoryType']; if (this.repositoryType === 'USER') { this.loadStudentParticipation(participationId); From 391b8b6ce4cae3e5f5ff9024f336ad69ee343539 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 16 Sep 2024 22:48:44 +0200 Subject: [PATCH 04/20] remove dependency on LocalVcService --- .../web/repository/AuxiliaryRepositoryResource.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index 40b0352a33ac..84de389867f5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -1,6 +1,7 @@ package de.tum.cit.aet.artemis.programming.web.repository; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.programming.service.localvc.LocalVCService.getDefaultBranchOfRepository; import java.security.Principal; import java.util.List; @@ -47,7 +48,7 @@ import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.RepositoryAccessService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; -import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCService; +import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; @@ -61,16 +62,12 @@ public class AuxiliaryRepositoryResource extends RepositoryResource { private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; - private final LocalVCService localVcService; - public AuxiliaryRepositoryResource(ProfileService profileService, UserRepository userRepository, AuthorizationCheckService authCheckService, GitService gitService, RepositoryService repositoryService, Optional versionControlService, ProgrammingExerciseRepository programmingExerciseRepository, - RepositoryAccessService repositoryAccessService, Optional localVCServletService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, - LocalVCService localVcService) { + RepositoryAccessService repositoryAccessService, Optional localVCServletService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository) { super(profileService, userRepository, authCheckService, gitService, repositoryService, versionControlService, programmingExerciseRepository, repositoryAccessService, localVCServletService); this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; - this.localVcService = localVcService; } @Override @@ -103,7 +100,8 @@ boolean canAccessRepository(Long auxiliaryRepositoryId) { @Override String getOrRetrieveBranchOfDomainObject(Long auxiliaryRepositoryId) { AuxiliaryRepository auxiliaryRep = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); - return localVcService.getDefaultBranchOfRepository(auxiliaryRep.getVcsRepositoryUri()); + LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(auxiliaryRep.getRepositoryUri()); + return getDefaultBranchOfRepository(localVCRepositoryUri); } @Override From 6ed98a4417c542cf7360b632b22d0a2d93729cd9 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 13:23:40 +0200 Subject: [PATCH 05/20] added suggestions --- .../artemis/programming/web/ProgrammingExerciseResource.java | 4 ++-- .../web/repository/AuxiliaryRepositoryResource.java | 4 ++-- .../services/programming-exercise-participation.service.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 9a29ed231896..67dd289da73c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -540,9 +540,9 @@ public ResponseEntity getProgrammingExerciseWithTemplateAnd */ @GetMapping("programming-exercises/{exerciseId}/with-auxiliary-repository/{auxiliaryRepositoryId}") @EnforceAtLeastTutorInExercise - public ResponseEntity getProgrammingExerciseWithTemplateAndSolutionParticipation(@PathVariable long exerciseId, @PathVariable long auxiliaryRepositoryId) { + public ResponseEntity getProgrammingExerciseWithAuxiliaryRepository(@PathVariable long exerciseId, @PathVariable long auxiliaryRepositoryId) { - log.debug("REST request to get programming exercise with template and solution participation : {}", exerciseId); + log.debug("REST request to get programming exercise with auxiliary repositories: {}", exerciseId); final var programmingExercise = programmingExerciseService.loadProgrammingExerciseWithAuxiliaryRepositories(exerciseId); if (programmingExercise.getAuxiliaryRepositories().stream().noneMatch(id -> id.getId() == auxiliaryRepositoryId)) { throw new BadRequestAlertException("The auxiliary repository Id does not belong to the exercise.", "Exercise", diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index 84de389867f5..57e44acf5d7e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -53,7 +53,7 @@ import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; /** - * Executes requested actions on the test repository of a programming exercise. Only available to TAs, Instructors and Admins. + * Executes requested actions on the auxiliary repository of a programming exercise. Only available to TAs, Instructors and Admins. */ @Profile(PROFILE_CORE) @RestController @@ -181,7 +181,7 @@ public ResponseEntity getStatus(@PathVariable Long auxiliar } /** - * Update a list of files in a test repository based on the submission's content. + * Update a list of files in an auxiliary repository based on the submission's content. * * @param auxiliaryRepositoryId of exercise to which the files belong * @param submissions information about the file updates diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index d9fdb4ee769c..b334bd18a08d 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -187,7 +187,7 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer } retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, repositoryType: string, auxiliaryRepositoryId: number): Observable { - const params: { [key: string]: number | string } = {}; + const params: { [key: string]: number } = {}; params['repositoryId'] = auxiliaryRepositoryId; return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`, { params: params }); } From 84313a1ec36aa1baf5be9aaa26ff4f8a0be208b5 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 15:01:25 +0200 Subject: [PATCH 06/20] added client tests --- ...gramming-exercise-participation.service.ts | 6 +++ .../localvc/commit-history.component.spec.ts | 21 ++++++++++ .../localvc/repository-view.component.spec.ts | 39 +++++++++++++++++++ ...gramming-exercise-participation.service.ts | 1 + .../mock-programming-exercise.service.ts | 2 + .../programming-exercise.service.spec.ts | 22 +++++++++++ 6 files changed, 91 insertions(+) diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index b334bd18a08d..5b437f9f2a6b 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -186,6 +186,12 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`); } + /** + * Get the commit history for a specific auxiliary repository + * @param exerciseId the exercise the repository belongs to + * @param repositoryType the repositories type + * @param auxiliaryRepositoryId the id of the repository + */ retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, repositoryType: string, auxiliaryRepositoryId: number): Observable { const params: { [key: string]: number } = {}; params['repositoryId'] = auxiliaryRepositoryId; diff --git a/src/test/javascript/spec/component/localvc/commit-history.component.spec.ts b/src/test/javascript/spec/component/localvc/commit-history.component.spec.ts index 8c3a628980a6..672cfc49982f 100644 --- a/src/test/javascript/spec/component/localvc/commit-history.component.spec.ts +++ b/src/test/javascript/spec/component/localvc/commit-history.component.spec.ts @@ -272,4 +272,25 @@ describe('CommitHistoryComponent', () => { expect(component.paramSub?.closed).toBeTrue(); expect(component.participationSub?.closed).toBeTrue(); }); + + it('should load auxiliary repository commits', () => { + setupComponent(); + activatedRoute.setParameters({ repositoryType: 'AUXILIARY', repositoryId: 5 }); + jest.spyOn(programmingExerciseParticipationService, 'retrieveCommitHistoryForAuxiliaryRepository').mockReturnValue(of(mockTestCommits)); + + // Trigger ngOnInit + component.ngOnInit(); + + // Expectations + expect(component.commits).toEqual(mockTestCommits); // Updated to reflect the correct order + expect(component.commits[0].result).toBeUndefined(); + expect(component.commits[1].result).toBeUndefined(); + + // Trigger ngOnDestroy + component.ngOnDestroy(); + + // Expect subscription to be unsubscribed + expect(component.paramSub?.closed).toBeTrue(); + expect(component.participationSub?.closed).toBeTrue(); + }); }); diff --git a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts index ff004d03c91b..3924968f0f0d 100644 --- a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts +++ b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts @@ -18,6 +18,7 @@ import { DueDateStat } from 'app/course/dashboards/due-date-stat.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { MockProfileService } from '../../helpers/mocks/service/mock-profile.service'; +import { AuxiliaryRepository } from 'app/entities/programming/programming-exercise-auxiliary-repository-model'; describe('RepositoryViewComponent', () => { let component: RepositoryViewComponent; @@ -170,6 +171,44 @@ describe('RepositoryViewComponent', () => { expect(component.paramSub?.closed).toBeTrue(); }); + it('should load AUXILIARY repository type', () => { + // Mock exercise and participation data + const mockAuxiliaryRepository: AuxiliaryRepository = { id: 5, repositoryUri: 'repositoryUri', checkoutDirectory: 'dir', name: 'AuxRepo', description: 'description' }; + const mockExercise: ProgrammingExercise = { + id: 1, + numberOfAssessmentsOfCorrectionRounds: [new DueDateStat()], + auxiliaryRepositories: [mockAuxiliaryRepository], + studentAssignedTeamIdComputed: true, + secondCorrectionEnabled: true, + }; + const mockExerciseResponse: HttpResponse = new HttpResponse({ body: mockExercise }); + const exerciseId = 1; + const auxiliaryRepositoryId = 5; + + activatedRoute.setParameters({ exerciseId: exerciseId, repositoryType: 'AUXILIARY', auxiliaryRepositoryId: auxiliaryRepositoryId }); + jest.spyOn(programmingExerciseService, 'findWithAuxiliaryRepository').mockReturnValue(of(mockExerciseResponse)); + + // Trigger ngOnInit + component.ngOnInit(); + + // Expect loadingParticipation to be false after loading + expect(component.loadingParticipation).toBeFalse(); + + // Expect exercise and participation to be set correctly + expect(component.exercise).toEqual(mockExercise); + expect(component.participation).toBeUndefined(); + + // Expect domainService method to be called with the correct arguments + expect(component.domainService.setDomain).toHaveBeenCalledWith([DomainType.AUXILIARY_REPOSITORY, mockAuxiliaryRepository]); + + // Trigger ngOnDestroy + component.ngOnDestroy(); + + // Expect subscription to be unsubscribed + expect(component.differentParticipationSub?.closed).toBeTrue(); + expect(component.paramSub?.closed).toBeTrue(); + }); + it('should handle unknown repository type', () => { // Mock exercise and participation data const mockExercise: ProgrammingExercise = { diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts index 472de3e839b5..00ecf361d725 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts @@ -11,6 +11,7 @@ export class MockProgrammingExerciseParticipationService implements IProgramming getStudentParticipationWithAllResults = (participationId: number) => of({} as ProgrammingExerciseStudentParticipation); retrieveCommitHistoryForParticipation = (participationId: number) => of([] as CommitInfo[]); retrieveCommitHistoryForTemplateSolutionOrTests = (participationId: number, repositoryType: string) => of([] as CommitInfo[]); + retrieveCommitHistoryForAuxiliaryRepository = (exerciseId: number, repositoryType: string, repositoryId: number) => of([] as CommitInfo[]); getParticipationRepositoryFilesWithContentAtCommitForCommitDetailsView = (exerciseId: number, participationId: number, commitId: string, repositoryType: string) => of(new Map()); checkIfParticipationHasResult = (participationId: number) => of(true); diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts index b164cc8e700c..5d320caff6c8 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts @@ -2,12 +2,14 @@ import { of } from 'rxjs'; import { ProgrammingExerciseInstructorRepositoryType } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { Participation } from 'app/entities/participation/participation.model'; import { ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model'; +import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; export class MockProgrammingExerciseService { updateProblemStatement = (exerciseId: number, problemStatement: string) => of(); findWithTemplateAndSolutionParticipation = (exerciseId: number) => of(); findWithTemplateAndSolutionParticipationAndResults = (exerciseId: number) => of(); findWithTemplateAndSolutionParticipationAndLatestResults = (exerciseId: number) => of(); + findWithAuxiliaryRepository = (programmingExerciseId: number, auxiliaryRepositoryId: number) => of(); find = (exerciseId: number) => of({ body: { id: 4 } }); getProgrammingExerciseTestCaseState = (exerciseId: number) => of({ body: { released: true, hasStudentResult: true, testCasesChanged: false } }); exportInstructorExercise = (exerciseId: number) => of({ body: undefined }); diff --git a/src/test/javascript/spec/service/programming-exercise.service.spec.ts b/src/test/javascript/spec/service/programming-exercise.service.spec.ts index 70d9a94a1eb7..4e298e77f324 100644 --- a/src/test/javascript/spec/service/programming-exercise.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise.service.spec.ts @@ -20,6 +20,7 @@ import { SolutionProgrammingExerciseParticipation } from 'app/entities/participa import { Submission } from 'app/entities/submission.model'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ProgrammingExerciseGitDiffEntry } from 'app/entities/hestia/programming-exercise-git-diff-entry.model'; +import { AuxiliaryRepository } from 'app/entities/programming/programming-exercise-auxiliary-repository-model'; describe('ProgrammingExercise Service', () => { let service: ProgrammingExerciseService; @@ -86,6 +87,27 @@ describe('ProgrammingExercise Service', () => { tick(); })); + it('should find with auxiliary repositories', fakeAsync(() => { + const auxiliaryRepository: AuxiliaryRepository = { id: 5 }; + const returnedFromService = { + ...defaultProgrammingExercise, + auxiliaryRepositories: [auxiliaryRepository], + releaseDate: undefined, + dueDate: undefined, + assessmentDueDate: undefined, + buildAndTestStudentSubmissionsAfterDueDate: undefined, + studentParticipations: [], + }; + const expected = { ...returnedFromService }; + service + .findWithAuxiliaryRepository(returnedFromService.id!, auxiliaryRepository.id!) + .pipe(take(1)) + .subscribe((resp) => expect(resp.body).toEqual(expected)); + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + tick(); + })); + it('should create a ProgrammingExercise', fakeAsync(() => { const returnedFromService = { ...defaultProgrammingExercise, From 0280f1f41df6f567deef3906f077b85c1744acaa Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 18:10:23 +0200 Subject: [PATCH 07/20] added initial AuxiliaryRepositoryResourceIntegrationTest --- ...iaryRepositoryResourceIntegrationTest.java | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java new file mode 100644 index 000000000000..9ab6b28f1a6c --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java @@ -0,0 +1,500 @@ +package de.tum.cit.aet.artemis.exercise.programming; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.io.FileUtils; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.merge.MergeStrategy; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; + +import de.tum.cit.aet.artemis.AbstractSpringIntegrationJenkinsGitlabTest; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.programming.domain.File; +import de.tum.cit.aet.artemis.programming.domain.FileType; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.domain.Repository; +import de.tum.cit.aet.artemis.programming.dto.FileMove; +import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; +import de.tum.cit.aet.artemis.programming.service.GitService; +import de.tum.cit.aet.artemis.programming.web.repository.FileSubmission; +import de.tum.cit.aet.artemis.util.GitUtilService; +import de.tum.cit.aet.artemis.util.LocalRepository; + +class AuxiliaryRepositoryResourceIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { + + private static final String TEST_PREFIX = "auxiliaryrepositoryresourceint"; + + @Autowired + private ProgrammingExerciseRepository programmingExerciseRepository; + + @Autowired + private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + + private final String testRepoBaseUrl = "/api/auxiliary-repository/"; + + private ProgrammingExercise programmingExercise; + + private final String currentLocalFileName = "currentFileName"; + + private final String currentLocalFileContent = "testContent"; + + private final String currentLocalFolderName = "currentFolderName"; + + private final LocalRepository testRepo = new LocalRepository(defaultBranch); + + @BeforeEach + void setup() throws Exception { + userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); + Course course = courseUtilService.addEmptyCourse(); + programmingExercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); + programmingExercise.setBuildConfig(programmingExerciseBuildConfigRepository.save(programmingExercise.getBuildConfig())); + + // Instantiate the remote repository as non-bare so its files can be manipulated + testRepo.configureRepos("testLocalRepo", "testOriginRepo", false); + + // add file to the repository folder + Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + var file = Files.createFile(filePath).toFile(); + // write content to the created file + FileUtils.write(file, currentLocalFileContent, Charset.defaultCharset()); + + // add folder to the repository folder + filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName); + Files.createDirectory(filePath); + + var testRepoUri = new GitUtilService.MockFileRepositoryUri(testRepo.localRepoFile); + programmingExercise.setTestRepositoryUri(testRepoUri.toString()); + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(testRepoUri, true); + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(testRepoUri, false); + + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(testRepoUri), eq(true), + any()); + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(testRepoUri), eq(false), + any()); + + doReturn(defaultBranch).when(versionControlService).getOrRetrieveBranchOfExercise(programmingExercise); + } + + @AfterEach + void tearDown() throws IOException { + reset(gitService); + testRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetFiles() throws Exception { + programmingExerciseRepository.save(programmingExercise); + var files = request.getMap(testRepoBaseUrl + programmingExercise.getId() + "/files", HttpStatus.OK, String.class, FileType.class); + assertThat(files).isNotEmpty(); + + // Check if all files exist + for (String key : files.keySet()) { + assertThat(Path.of(testRepo.localRepoFile + "/" + key)).exists(); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "STUDENT") + void testGetFilesAsStudent_accessForbidden() throws Exception { + programmingExerciseRepository.save(programmingExercise); + request.getMap(testRepoBaseUrl + programmingExercise.getId() + "/files", HttpStatus.FORBIDDEN, String.class, FileType.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("file", currentLocalFileName); + var file = request.get(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, byte[].class, params); + assertThat(file).isNotEmpty(); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(new String(file)).isEqualTo(currentLocalFileContent); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/newFile")).doesNotExist(); + params.add("file", "newFile"); + request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); + assertThat(Path.of(testRepo.localRepoFile + "/newFile")).isRegularFile(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateFile_alreadyExists() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat((Path.of(testRepo.localRepoFile + "/newFile"))).doesNotExist(); + params.add("file", "newFile"); + + doReturn(Optional.of(true)).when(gitService).getFileByName(any(), any()); + request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateFile_invalidRepository() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat((Path.of(testRepo.localRepoFile + "/newFile"))).doesNotExist(); + params.add("file", "newFile"); + + Repository mockRepository = mock(Repository.class); + doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); + doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(false).when(mockRepository).isValidFile(any()); + request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCreateFolder() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/newFolder")).doesNotExist(); + params.add("folder", "newFolder"); + request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/folder", HttpStatus.OK, params); + assertThat(Path.of(testRepo.localRepoFile + "/newFolder")).isDirectory(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRenameFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + assertThat((Path.of(testRepo.localRepoFile + "/" + currentLocalFileName))).exists(); + String newLocalFileName = "newFileName"; + assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); + FileMove fileMove = new FileMove(currentLocalFileName, newLocalFileName); + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.OK, null); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); + assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).exists(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRenameFile_alreadyExists() throws Exception { + programmingExerciseRepository.save(programmingExercise); + FileMove fileMove = createRenameFileMove(); + + doReturn(Optional.empty()).when(gitService).getFileByName(any(), any()); + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.NOT_FOUND, null); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRenameFile_invalidExistingFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + FileMove fileMove = createRenameFileMove(); + + doReturn(Optional.of(testRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(fileMove.currentFilePath())); + + Repository mockRepository = mock(Repository.class); + doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); + doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(false).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.BAD_REQUEST, null); + } + + private FileMove createRenameFileMove() { + String newLocalFileName = "newFileName"; + + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); + + return new FileMove(currentLocalFileName, newLocalFileName); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testRenameFolder() throws Exception { + programmingExerciseRepository.save(programmingExercise); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName)).exists(); + String newLocalFolderName = "newFolderName"; + assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFolderName)).doesNotExist(); + FileMove fileMove = new FileMove(currentLocalFolderName, newLocalFolderName); + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.OK, null); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName)).doesNotExist(); + assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFolderName)).exists(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + params.add("file", currentLocalFileName); + request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteFile_notFound() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + params.add("file", currentLocalFileName); + + doReturn(Optional.empty()).when(gitService).getFileByName(any(), any()); + + request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.NOT_FOUND, params); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteFile_invalidFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + params.add("file", currentLocalFileName); + + doReturn(Optional.of(testRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(currentLocalFileName)); + + Repository mockRepository = mock(Repository.class); + doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); + doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(false).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); + + request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteFile_validFile() throws Exception { + programmingExerciseRepository.save(programmingExercise); + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + params.add("file", currentLocalFileName); + + File mockFile = mock(File.class); + doReturn(Optional.of(mockFile)).when(gitService).getFileByName(any(), eq(currentLocalFileName)); + doReturn(currentLocalFileName).when(mockFile).getName(); + doReturn(false).when(mockFile).isFile(); + + Repository mockRepository = mock(Repository.class); + doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); + doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(true).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); + + request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testCommitChanges() throws Exception { + programmingExerciseRepository.save(programmingExercise); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); + var testRepoCommits = testRepo.getAllLocalCommits(); + assertThat(testRepoCommits).hasSize(1); + assertThat(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()).isEqualTo(testRepoCommits.getFirst().getAuthorIdent().getName()); + } + + private List getFileSubmissions() { + List fileSubmissions = new ArrayList<>(); + FileSubmission fileSubmission = new FileSubmission(); + fileSubmission.setFileName(currentLocalFileName); + fileSubmission.setFileContent("updatedFileContent"); + fileSubmissions.add(fileSubmission); + return fileSubmissions; + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testSaveFiles() throws Exception { + programmingExerciseRepository.save(programmingExercise); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=false", getFileSubmissions(), HttpStatus.OK); + + Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + assertThat(filePath).hasContent("updatedFileContent"); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testSaveFilesAndCommit() throws Exception { + programmingExerciseRepository.save(programmingExercise); + assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); + + request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=true", getFileSubmissions(), HttpStatus.OK); + + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); + + Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + assertThat(filePath).hasContent("updatedFileContent"); + + var testRepoCommits = testRepo.getAllLocalCommits(); + assertThat(testRepoCommits).hasSize(1); + assertThat(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()).isEqualTo(testRepoCommits.getFirst().getAuthorIdent().getName()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "INSTRUCTOR") + void testSaveFiles_accessForbidden() throws Exception { + programmingExerciseRepository.save(programmingExercise); + // student1 should not have access to instructor1's tests repository even if they assume an INSTRUCTOR role. + request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=true", List.of(), HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testPullChanges() throws Exception { + programmingExerciseRepository.save(programmingExercise); + String fileName = "remoteFile"; + + // Create a commit for the local and the remote repository + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + try (var remoteRepository = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.originRepoFile.toPath(), null)) { + + // Create file in the remote repository + Path filePath = Path.of(testRepo.originRepoFile + "/" + fileName); + Files.createFile(filePath); + + // Check if the file exists in the remote repository and that it doesn't yet exist in the local repository + assertThat(Path.of(testRepo.originRepoFile + "/" + fileName)).exists(); + assertThat(Path.of(testRepo.localRepoFile + "/" + fileName)).doesNotExist(); + + // Stage all changes and make a second commit in the remote repository + gitService.stageAllChanges(remoteRepository); + GitService.commit(testRepo.originGit).setMessage("TestCommit").setAllowEmpty(true).setCommitter("testname", "test@email").call(); + + // Checks if the current commit is not equal on the local and the remote repository + assertThat(testRepo.getAllLocalCommits().getFirst()).isNotEqualTo(testRepo.getAllOriginCommits().getFirst()); + + // Execute the Rest call + request.get(testRepoBaseUrl + programmingExercise.getId() + "/pull", HttpStatus.OK, Void.class); + + // Check if the current commit is the same on the local and the remote repository and if the file exists on the local repository + assertThat(testRepo.getAllLocalCommits().getFirst()).isEqualTo(testRepo.getAllOriginCommits().getFirst()); + assertThat(Path.of(testRepo.localRepoFile + "/" + fileName)).exists(); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testResetToLastCommit() throws Exception { + programmingExerciseRepository.save(programmingExercise); + String fileName = "testFile"; + try (var localRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null); + var remoteRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.originRepoFile.toPath(), null)) { + + // Check status of git before the commit + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); + + // Create a commit for the local and the remote repository + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + + // Check status of git after the commit + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); + + // Create file in the local repository and commit it + Path localFilePath = Path.of(testRepo.localRepoFile + "/" + fileName); + var localFile = Files.createFile(localFilePath).toFile(); + // write content to the created file + FileUtils.write(localFile, "local", Charset.defaultCharset()); + gitService.stageAllChanges(localRepo); + GitService.commit(testRepo.localGit).setMessage("local").call(); + + // Create file in the remote repository and commit it + Path remoteFilePath = Path.of(testRepo.originRepoFile + "/" + fileName); + var remoteFile = Files.createFile(remoteFilePath).toFile(); + // write content to the created file + FileUtils.write(remoteFile, "remote", Charset.defaultCharset()); + gitService.stageAllChanges(remoteRepo); + GitService.commit(testRepo.originGit).setMessage("remote").call(); + + // Merge the two and a conflict will occur + testRepo.localGit.fetch().setRemote("origin").call(); + List refs = testRepo.localGit.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); + var result = testRepo.localGit.merge().include(refs.getFirst().getObjectId()).setStrategy(MergeStrategy.RESOLVE).call(); + var status = testRepo.localGit.status().call(); + assertThat(status.getConflicting()).isNotEmpty(); + assertThat(result.getMergeStatus()).isEqualTo(MergeResult.MergeStatus.CONFLICTING); + + // Execute the reset Rest call + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/reset", null, HttpStatus.OK, null); + + // Check the git status after the reset + status = testRepo.localGit.status().call(); + assertThat(status.getConflicting()).isEmpty(); + assertThat(testRepo.getAllLocalCommits().getFirst()).isEqualTo(testRepo.getAllOriginCommits().getFirst()); + var receivedStatusAfterReset = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusAfterReset.repositoryStatus()).hasToString("CLEAN"); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetStatus() throws Exception { + programmingExerciseRepository.save(programmingExercise); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + + // The current status is "uncommited changes", since we added files and folders, but we didn't commit yet + assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); + + // Perform a commit to check if the status changes + request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + + // Check if the status of git is "clean" after the commit + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "INSTRUCTOR") + void testGetStatus_cannotAccessRepository() throws Exception { + programmingExerciseRepository.save(programmingExercise); + // student1 should not have access to instructor1's tests repository even if they assume the role of an INSTRUCTOR. + request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.FORBIDDEN, RepositoryStatusDTO.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testIsClean() throws Exception { + programmingExerciseRepository.save(programmingExercise); + doReturn(true).when(gitService).isRepositoryCached(any()); + var status = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, Map.class); + assertThat(status).isNotEmpty(); + } +} From 304caa6cabf9160af64f61be3be41cc04cda9e17 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 19:44:03 +0200 Subject: [PATCH 08/20] added java integration test for AuxiliaryRepositoryResource --- .../AuxiliaryRepositoryResource.java | 8 +- ...iaryRepositoryResourceIntegrationTest.java | 213 ++++++++++-------- 2 files changed, 119 insertions(+), 102 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index 57e44acf5d7e..ef384402de76 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -1,7 +1,6 @@ package de.tum.cit.aet.artemis.programming.web.repository; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import static de.tum.cit.aet.artemis.programming.service.localvc.LocalVCService.getDefaultBranchOfRepository; import java.security.Principal; import java.util.List; @@ -48,7 +47,6 @@ import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.RepositoryAccessService; import de.tum.cit.aet.artemis.programming.service.RepositoryService; -import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService; @@ -99,9 +97,9 @@ boolean canAccessRepository(Long auxiliaryRepositoryId) { @Override String getOrRetrieveBranchOfDomainObject(Long auxiliaryRepositoryId) { - AuxiliaryRepository auxiliaryRep = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); - LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(auxiliaryRep.getRepositoryUri()); - return getDefaultBranchOfRepository(localVCRepositoryUri); + AuxiliaryRepository auxiliaryRepo = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); + ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(auxiliaryRepo.getExercise().getId()); + return versionControlService.orElseThrow().getOrRetrieveBranchOfExercise(exercise); } @Override diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java index 9ab6b28f1a6c..184cbd1991ec 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java @@ -33,12 +33,14 @@ import de.tum.cit.aet.artemis.AbstractSpringIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.File; import de.tum.cit.aet.artemis.programming.domain.FileType; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.Repository; import de.tum.cit.aet.artemis.programming.dto.FileMove; import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; +import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.service.GitService; @@ -56,17 +58,22 @@ class AuxiliaryRepositoryResourceIntegrationTest extends AbstractSpringIntegrati @Autowired private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + @Autowired + private AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + private final String testRepoBaseUrl = "/api/auxiliary-repository/"; private ProgrammingExercise programmingExercise; + private AuxiliaryRepository auxiliaryRepository; + private final String currentLocalFileName = "currentFileName"; private final String currentLocalFileContent = "testContent"; private final String currentLocalFolderName = "currentFolderName"; - private final LocalRepository testRepo = new LocalRepository(defaultBranch); + private final LocalRepository localAuxiliaryRepo = new LocalRepository(defaultBranch); @BeforeEach void setup() throws Exception { @@ -76,27 +83,39 @@ void setup() throws Exception { programmingExercise.setBuildConfig(programmingExerciseBuildConfigRepository.save(programmingExercise.getBuildConfig())); // Instantiate the remote repository as non-bare so its files can be manipulated - testRepo.configureRepos("testLocalRepo", "testOriginRepo", false); + localAuxiliaryRepo.configureRepos("auxLocalRepo", "auxOriginRepo", false); // add file to the repository folder - Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + Path filePath = Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName); var file = Files.createFile(filePath).toFile(); // write content to the created file FileUtils.write(file, currentLocalFileContent, Charset.defaultCharset()); // add folder to the repository folder - filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName); + filePath = Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFolderName); Files.createDirectory(filePath); - var testRepoUri = new GitUtilService.MockFileRepositoryUri(testRepo.localRepoFile); - programmingExercise.setTestRepositoryUri(testRepoUri.toString()); - doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(testRepoUri, true); - doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(testRepoUri, false); - - doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(testRepoUri), eq(true), - any()); - doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(testRepoUri), eq(false), - any()); + // add the auxiliary repository + auxiliaryRepositoryRepository.deleteAll(); + var auxRepoUri = new GitUtilService.MockFileRepositoryUri(localAuxiliaryRepo.localRepoFile); + programmingExercise.setTestRepositoryUri(auxRepoUri.toString()); + var newAuxiliaryRepo = new AuxiliaryRepository(); + newAuxiliaryRepo.setName("AuxiliaryRepo"); + newAuxiliaryRepo.setRepositoryUri(auxRepoUri.toString()); + newAuxiliaryRepo.setCheckoutDirectory(localAuxiliaryRepo.localRepoFile.toPath().toString()); + newAuxiliaryRepo.setExercise(programmingExercise); + programmingExercise.setAuxiliaryRepositories(List.of(newAuxiliaryRepo)); + programmingExercise = programmingExerciseRepository.save(programmingExercise); + auxiliaryRepository = programmingExercise.getAuxiliaryRepositories().getFirst(); + + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(auxRepoUri, true); + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(auxRepoUri, + false); + + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(auxRepoUri), + eq(true), any()); + doReturn(gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.localRepoFile.toPath(), null)).when(gitService).getOrCheckoutRepository(eq(auxRepoUri), + eq(false), any()); doReturn(defaultBranch).when(versionControlService).getOrRetrieveBranchOfExercise(programmingExercise); } @@ -104,19 +123,19 @@ void setup() throws Exception { @AfterEach void tearDown() throws IOException { reset(gitService); - testRepo.resetLocalRepo(); + localAuxiliaryRepo.resetLocalRepo(); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetFiles() throws Exception { programmingExerciseRepository.save(programmingExercise); - var files = request.getMap(testRepoBaseUrl + programmingExercise.getId() + "/files", HttpStatus.OK, String.class, FileType.class); + var files = request.getMap(testRepoBaseUrl + auxiliaryRepository.getId() + "/files", HttpStatus.OK, String.class, FileType.class); assertThat(files).isNotEmpty(); // Check if all files exist for (String key : files.keySet()) { - assertThat(Path.of(testRepo.localRepoFile + "/" + key)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + key)).exists(); } } @@ -124,7 +143,7 @@ void testGetFiles() throws Exception { @WithMockUser(username = TEST_PREFIX + "student1", roles = "STUDENT") void testGetFilesAsStudent_accessForbidden() throws Exception { programmingExerciseRepository.save(programmingExercise); - request.getMap(testRepoBaseUrl + programmingExercise.getId() + "/files", HttpStatus.FORBIDDEN, String.class, FileType.class); + request.getMap(testRepoBaseUrl + auxiliaryRepository.getId() + "/files", HttpStatus.FORBIDDEN, String.class, FileType.class); } @Test @@ -133,9 +152,9 @@ void testGetFile() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("file", currentLocalFileName); - var file = request.get(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, byte[].class, params); + var file = request.get(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.OK, byte[].class, params); assertThat(file).isNotEmpty(); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); assertThat(new String(file)).isEqualTo(currentLocalFileContent); } @@ -144,10 +163,10 @@ void testGetFile() throws Exception { void testCreateFile() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/newFile")).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/newFile")).doesNotExist(); params.add("file", "newFile"); - request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); - assertThat(Path.of(testRepo.localRepoFile + "/newFile")).isRegularFile(); + request.postWithoutResponseBody(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.OK, params); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/newFile")).isRegularFile(); } @Test @@ -155,7 +174,7 @@ void testCreateFile() throws Exception { void testCreateFile_alreadyExists() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat((Path.of(testRepo.localRepoFile + "/newFile"))).doesNotExist(); + assertThat((Path.of(localAuxiliaryRepo.localRepoFile + "/newFile"))).doesNotExist(); params.add("file", "newFile"); doReturn(Optional.of(true)).when(gitService).getFileByName(any(), any()); @@ -167,14 +186,14 @@ void testCreateFile_alreadyExists() throws Exception { void testCreateFile_invalidRepository() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat((Path.of(testRepo.localRepoFile + "/newFile"))).doesNotExist(); + assertThat((Path.of(localAuxiliaryRepo.localRepoFile + "/newFile"))).doesNotExist(); params.add("file", "newFile"); Repository mockRepository = mock(Repository.class); doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); - doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(localAuxiliaryRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); doReturn(false).when(mockRepository).isValidFile(any()); - request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + request.postWithoutResponseBody(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.BAD_REQUEST, params); } @Test @@ -182,23 +201,23 @@ void testCreateFile_invalidRepository() throws Exception { void testCreateFolder() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/newFolder")).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/newFolder")).doesNotExist(); params.add("folder", "newFolder"); - request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/folder", HttpStatus.OK, params); - assertThat(Path.of(testRepo.localRepoFile + "/newFolder")).isDirectory(); + request.postWithoutResponseBody(testRepoBaseUrl + auxiliaryRepository.getId() + "/folder", HttpStatus.OK, params); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/newFolder")).isDirectory(); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testRenameFile() throws Exception { programmingExerciseRepository.save(programmingExercise); - assertThat((Path.of(testRepo.localRepoFile + "/" + currentLocalFileName))).exists(); + assertThat((Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName))).exists(); String newLocalFileName = "newFileName"; - assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); FileMove fileMove = new FileMove(currentLocalFileName, newLocalFileName); - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.OK, null); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); - assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).exists(); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/rename-file", fileMove, HttpStatus.OK, null); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + newLocalFileName)).exists(); } @Test @@ -208,7 +227,7 @@ void testRenameFile_alreadyExists() throws Exception { FileMove fileMove = createRenameFileMove(); doReturn(Optional.empty()).when(gitService).getFileByName(any(), any()); - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.NOT_FOUND, null); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/rename-file", fileMove, HttpStatus.NOT_FOUND, null); } @Test @@ -217,20 +236,20 @@ void testRenameFile_invalidExistingFile() throws Exception { programmingExerciseRepository.save(programmingExercise); FileMove fileMove = createRenameFileMove(); - doReturn(Optional.of(testRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(fileMove.currentFilePath())); + doReturn(Optional.of(localAuxiliaryRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(fileMove.currentFilePath())); Repository mockRepository = mock(Repository.class); doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); - doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(localAuxiliaryRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); doReturn(false).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.BAD_REQUEST, null); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/rename-file", fileMove, HttpStatus.BAD_REQUEST, null); } private FileMove createRenameFileMove() { String newLocalFileName = "newFileName"; - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); - assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + newLocalFileName)).doesNotExist(); return new FileMove(currentLocalFileName, newLocalFileName); } @@ -239,13 +258,13 @@ private FileMove createRenameFileMove() { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testRenameFolder() throws Exception { programmingExerciseRepository.save(programmingExercise); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFolderName)).exists(); String newLocalFolderName = "newFolderName"; - assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFolderName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + newLocalFolderName)).doesNotExist(); FileMove fileMove = new FileMove(currentLocalFolderName, newLocalFolderName); - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/rename-file", fileMove, HttpStatus.OK, null); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFolderName)).doesNotExist(); - assertThat(Path.of(testRepo.localRepoFile + "/" + newLocalFolderName)).exists(); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/rename-file", fileMove, HttpStatus.OK, null); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFolderName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + newLocalFolderName)).exists(); } @Test @@ -253,10 +272,10 @@ void testRenameFolder() throws Exception { void testDeleteFile() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); params.add("file", currentLocalFileName); - request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); + request.delete(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.OK, params); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).doesNotExist(); } @Test @@ -264,7 +283,7 @@ void testDeleteFile() throws Exception { void testDeleteFile_notFound() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); params.add("file", currentLocalFileName); doReturn(Optional.empty()).when(gitService).getFileByName(any(), any()); @@ -277,17 +296,17 @@ void testDeleteFile_notFound() throws Exception { void testDeleteFile_invalidFile() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); params.add("file", currentLocalFileName); - doReturn(Optional.of(testRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(currentLocalFileName)); + doReturn(Optional.of(localAuxiliaryRepo.localRepoFile)).when(gitService).getFileByName(any(), eq(currentLocalFileName)); Repository mockRepository = mock(Repository.class); doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); - doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(localAuxiliaryRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); doReturn(false).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); - request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + request.delete(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.BAD_REQUEST, params); } @Test @@ -295,7 +314,7 @@ void testDeleteFile_invalidFile() throws Exception { void testDeleteFile_validFile() throws Exception { programmingExerciseRepository.save(programmingExercise); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); params.add("file", currentLocalFileName); File mockFile = mock(File.class); @@ -305,22 +324,22 @@ void testDeleteFile_validFile() throws Exception { Repository mockRepository = mock(Repository.class); doReturn(mockRepository).when(gitService).getOrCheckoutRepository(any(), eq(true)); - doReturn(testRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); + doReturn(localAuxiliaryRepo.localRepoFile.toPath()).when(mockRepository).getLocalPath(); doReturn(true).when(mockRepository).isValidFile(argThat(file -> file.getName().contains(currentLocalFileName))); - request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.OK, params); + request.delete(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.OK, params); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCommitChanges() throws Exception { programmingExerciseRepository.save(programmingExercise); - var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); - var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/commit", null, HttpStatus.OK, null); + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); - var testRepoCommits = testRepo.getAllLocalCommits(); + var testRepoCommits = localAuxiliaryRepo.getAllLocalCommits(); assertThat(testRepoCommits).hasSize(1); assertThat(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()).isEqualTo(testRepoCommits.getFirst().getAuthorIdent().getName()); } @@ -338,10 +357,10 @@ private List getFileSubmissions() { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testSaveFiles() throws Exception { programmingExerciseRepository.save(programmingExercise); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); - request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=false", getFileSubmissions(), HttpStatus.OK); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + request.put(testRepoBaseUrl + auxiliaryRepository.getId() + "/files?commit=false", getFileSubmissions(), HttpStatus.OK); - Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + Path filePath = Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName); assertThat(filePath).hasContent("updatedFileContent"); } @@ -349,20 +368,20 @@ void testSaveFiles() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testSaveFilesAndCommit() throws Exception { programmingExerciseRepository.save(programmingExercise); - assertThat(Path.of(testRepo.localRepoFile + "/" + currentLocalFileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName)).exists(); - var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); - request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=true", getFileSubmissions(), HttpStatus.OK); + request.put(testRepoBaseUrl + auxiliaryRepository.getId() + "/files?commit=true", getFileSubmissions(), HttpStatus.OK); - var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); - Path filePath = Path.of(testRepo.localRepoFile + "/" + currentLocalFileName); + Path filePath = Path.of(localAuxiliaryRepo.localRepoFile + "/" + currentLocalFileName); assertThat(filePath).hasContent("updatedFileContent"); - var testRepoCommits = testRepo.getAllLocalCommits(); + var testRepoCommits = localAuxiliaryRepo.getAllLocalCommits(); assertThat(testRepoCommits).hasSize(1); assertThat(userUtilService.getUserByLogin(TEST_PREFIX + "instructor1").getName()).isEqualTo(testRepoCommits.getFirst().getAuthorIdent().getName()); } @@ -372,7 +391,7 @@ void testSaveFilesAndCommit() throws Exception { void testSaveFiles_accessForbidden() throws Exception { programmingExerciseRepository.save(programmingExercise); // student1 should not have access to instructor1's tests repository even if they assume an INSTRUCTOR role. - request.put(testRepoBaseUrl + programmingExercise.getId() + "/files?commit=true", List.of(), HttpStatus.FORBIDDEN); + request.put(testRepoBaseUrl + auxiliaryRepository.getId() + "/files?commit=true", List.of(), HttpStatus.FORBIDDEN); } @Test @@ -382,30 +401,30 @@ void testPullChanges() throws Exception { String fileName = "remoteFile"; // Create a commit for the local and the remote repository - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); - try (var remoteRepository = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.originRepoFile.toPath(), null)) { + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/commit", null, HttpStatus.OK, null); + try (var remoteRepository = gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.originRepoFile.toPath(), null)) { // Create file in the remote repository - Path filePath = Path.of(testRepo.originRepoFile + "/" + fileName); + Path filePath = Path.of(localAuxiliaryRepo.originRepoFile + "/" + fileName); Files.createFile(filePath); // Check if the file exists in the remote repository and that it doesn't yet exist in the local repository - assertThat(Path.of(testRepo.originRepoFile + "/" + fileName)).exists(); - assertThat(Path.of(testRepo.localRepoFile + "/" + fileName)).doesNotExist(); + assertThat(Path.of(localAuxiliaryRepo.originRepoFile + "/" + fileName)).exists(); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + fileName)).doesNotExist(); // Stage all changes and make a second commit in the remote repository gitService.stageAllChanges(remoteRepository); - GitService.commit(testRepo.originGit).setMessage("TestCommit").setAllowEmpty(true).setCommitter("testname", "test@email").call(); + GitService.commit(localAuxiliaryRepo.originGit).setMessage("TestCommit").setAllowEmpty(true).setCommitter("testname", "test@email").call(); // Checks if the current commit is not equal on the local and the remote repository - assertThat(testRepo.getAllLocalCommits().getFirst()).isNotEqualTo(testRepo.getAllOriginCommits().getFirst()); + assertThat(localAuxiliaryRepo.getAllLocalCommits().getFirst()).isNotEqualTo(localAuxiliaryRepo.getAllOriginCommits().getFirst()); // Execute the Rest call request.get(testRepoBaseUrl + programmingExercise.getId() + "/pull", HttpStatus.OK, Void.class); // Check if the current commit is the same on the local and the remote repository and if the file exists on the local repository - assertThat(testRepo.getAllLocalCommits().getFirst()).isEqualTo(testRepo.getAllOriginCommits().getFirst()); - assertThat(Path.of(testRepo.localRepoFile + "/" + fileName)).exists(); + assertThat(localAuxiliaryRepo.getAllLocalCommits().getFirst()).isEqualTo(localAuxiliaryRepo.getAllOriginCommits().getFirst()); + assertThat(Path.of(localAuxiliaryRepo.localRepoFile + "/" + fileName)).exists(); } } @@ -414,8 +433,8 @@ void testPullChanges() throws Exception { void testResetToLastCommit() throws Exception { programmingExerciseRepository.save(programmingExercise); String fileName = "testFile"; - try (var localRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.localRepoFile.toPath(), null); - var remoteRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(testRepo.originRepoFile.toPath(), null)) { + try (var localRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.localRepoFile.toPath(), null); + var remoteRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.originRepoFile.toPath(), null)) { // Check status of git before the commit var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); @@ -429,37 +448,37 @@ void testResetToLastCommit() throws Exception { assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); // Create file in the local repository and commit it - Path localFilePath = Path.of(testRepo.localRepoFile + "/" + fileName); + Path localFilePath = Path.of(localAuxiliaryRepo.localRepoFile + "/" + fileName); var localFile = Files.createFile(localFilePath).toFile(); // write content to the created file FileUtils.write(localFile, "local", Charset.defaultCharset()); gitService.stageAllChanges(localRepo); - GitService.commit(testRepo.localGit).setMessage("local").call(); + GitService.commit(localAuxiliaryRepo.localGit).setMessage("local").call(); // Create file in the remote repository and commit it - Path remoteFilePath = Path.of(testRepo.originRepoFile + "/" + fileName); + Path remoteFilePath = Path.of(localAuxiliaryRepo.originRepoFile + "/" + fileName); var remoteFile = Files.createFile(remoteFilePath).toFile(); // write content to the created file FileUtils.write(remoteFile, "remote", Charset.defaultCharset()); gitService.stageAllChanges(remoteRepo); - GitService.commit(testRepo.originGit).setMessage("remote").call(); + GitService.commit(localAuxiliaryRepo.originGit).setMessage("remote").call(); // Merge the two and a conflict will occur - testRepo.localGit.fetch().setRemote("origin").call(); - List refs = testRepo.localGit.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); - var result = testRepo.localGit.merge().include(refs.getFirst().getObjectId()).setStrategy(MergeStrategy.RESOLVE).call(); - var status = testRepo.localGit.status().call(); + localAuxiliaryRepo.localGit.fetch().setRemote("origin").call(); + List refs = localAuxiliaryRepo.localGit.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call(); + var result = localAuxiliaryRepo.localGit.merge().include(refs.getFirst().getObjectId()).setStrategy(MergeStrategy.RESOLVE).call(); + var status = localAuxiliaryRepo.localGit.status().call(); assertThat(status.getConflicting()).isNotEmpty(); assertThat(result.getMergeStatus()).isEqualTo(MergeResult.MergeStatus.CONFLICTING); // Execute the reset Rest call - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/reset", null, HttpStatus.OK, null); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/reset", null, HttpStatus.OK, null); // Check the git status after the reset - status = testRepo.localGit.status().call(); + status = localAuxiliaryRepo.localGit.status().call(); assertThat(status.getConflicting()).isEmpty(); - assertThat(testRepo.getAllLocalCommits().getFirst()).isEqualTo(testRepo.getAllOriginCommits().getFirst()); - var receivedStatusAfterReset = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + assertThat(localAuxiliaryRepo.getAllLocalCommits().getFirst()).isEqualTo(localAuxiliaryRepo.getAllOriginCommits().getFirst()); + var receivedStatusAfterReset = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusAfterReset.repositoryStatus()).hasToString("CLEAN"); } } @@ -468,13 +487,13 @@ void testResetToLastCommit() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetStatus() throws Exception { programmingExerciseRepository.save(programmingExercise); - var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); // The current status is "uncommited changes", since we added files and folders, but we didn't commit yet assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); // Perform a commit to check if the status changes - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/commit", null, HttpStatus.OK, null); // Check if the status of git is "clean" after the commit var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); @@ -486,7 +505,7 @@ void testGetStatus() throws Exception { void testGetStatus_cannotAccessRepository() throws Exception { programmingExerciseRepository.save(programmingExercise); // student1 should not have access to instructor1's tests repository even if they assume the role of an INSTRUCTOR. - request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.FORBIDDEN, RepositoryStatusDTO.class); + request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.FORBIDDEN, RepositoryStatusDTO.class); } @Test @@ -494,7 +513,7 @@ void testGetStatus_cannotAccessRepository() throws Exception { void testIsClean() throws Exception { programmingExerciseRepository.save(programmingExercise); doReturn(true).when(gitService).isRepositoryCached(any()); - var status = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, Map.class); + var status = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, Map.class); assertThat(status).isNotEmpty(); } } From f9a34724ef067ff8772f3d69b5420c405043e441 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 20:13:48 +0200 Subject: [PATCH 09/20] adjusted endpoint --- .../web/repository/AuxiliaryRepositoryResource.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index ef384402de76..b0eb38f9e8fb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -195,13 +195,13 @@ public ResponseEntity> updateTestFiles(@PathVariable("auxili if (versionControlService.isEmpty()) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "VCSNotPresent"); } - - ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(auxiliaryRepositoryId); + AuxiliaryRepository auxiliaryRepository = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); + ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(auxiliaryRepository.getExercise().getId()); Repository repository; try { repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(true, exercise, userRepository.getUserWithGroupsAndAuthorities(principal.getName()), "test"); - repository = gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUri(), true); + repository = gitService.getOrCheckoutRepository(auxiliaryRepository.getVcsRepositoryUri(), true); } catch (AccessForbiddenException e) { FileSubmissionError error = new FileSubmissionError(auxiliaryRepositoryId, "noPermissions"); From fdac5df770b55275b589361d5e64a5c2b543f285 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 20:46:16 +0200 Subject: [PATCH 10/20] added improvements --- .../AuxiliaryRepositoryResource.java | 32 +++++++++---------- ...gramming-exercise-participation.service.ts | 4 +-- .../commit-history.component.ts | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java index b0eb38f9e8fb..8c03cf7dae19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/repository/AuxiliaryRepositoryResource.java @@ -55,7 +55,7 @@ */ @Profile(PROFILE_CORE) @RestController -@RequestMapping("api/") +@RequestMapping("api/auxiliary-repository/") public class AuxiliaryRepositoryResource extends RepositoryResource { private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; @@ -72,7 +72,7 @@ public AuxiliaryRepositoryResource(ProfileService profileService, UserRepository Repository getRepository(Long auxiliaryRepositoryId, RepositoryActionType repositoryActionType, boolean pullOnGet) throws GitAPIException { final var auxiliaryRepository = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); User user = userRepository.getUserWithGroupsAndAuthorities(); - repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, auxiliaryRepository.getExercise(), user, "test"); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, auxiliaryRepository.getExercise(), user, "auxiliary"); final var repoUri = auxiliaryRepository.getVcsRepositoryUri(); return gitService.getOrCheckoutRepository(repoUri, pullOnGet); } @@ -103,21 +103,21 @@ String getOrRetrieveBranchOfDomainObject(Long auxiliaryRepositoryId) { } @Override - @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/files", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "{auxiliaryRepositoryId}/files", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity> getFiles(@PathVariable Long auxiliaryRepositoryId) { return super.getFiles(auxiliaryRepositoryId); } @Override - @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @GetMapping(value = "{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @EnforceAtLeastTutor public ResponseEntity getFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filename) { return super.getFile(auxiliaryRepositoryId, filename); } @Override - @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity createFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filePath, HttpServletRequest request) { @@ -125,7 +125,7 @@ public ResponseEntity createFile(@PathVariable Long auxiliaryRepositoryId, } @Override - @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "{auxiliaryRepositoryId}/folder", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity createFolder(@PathVariable Long auxiliaryRepositoryId, @RequestParam("folder") String folderPath, HttpServletRequest request) { @@ -133,7 +133,7 @@ public ResponseEntity createFolder(@PathVariable Long auxiliaryRepositoryI } @Override - @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "{auxiliaryRepositoryId}/rename-file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity renameFile(@PathVariable Long auxiliaryRepositoryId, @RequestBody FileMove fileMove) { @@ -141,7 +141,7 @@ public ResponseEntity renameFile(@PathVariable Long auxiliaryRepositoryId, } @Override - @DeleteMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) + @DeleteMapping(value = "{auxiliaryRepositoryId}/file", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity deleteFile(@PathVariable Long auxiliaryRepositoryId, @RequestParam("file") String filename) { @@ -149,14 +149,14 @@ public ResponseEntity deleteFile(@PathVariable Long auxiliaryRepositoryId, } @Override - @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "{auxiliaryRepositoryId}/pull", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity pullChanges(@PathVariable Long auxiliaryRepositoryId) { return super.pullChanges(auxiliaryRepositoryId); } @Override - @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "{auxiliaryRepositoryId}/commit", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity commitChanges(@PathVariable Long auxiliaryRepositoryId) { @@ -164,7 +164,7 @@ public ResponseEntity commitChanges(@PathVariable Long auxiliaryRepository } @Override - @PostMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "{auxiliaryRepositoryId}/reset", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity resetToLastCommit(@PathVariable Long auxiliaryRepositoryId) { @@ -172,7 +172,7 @@ public ResponseEntity resetToLastCommit(@PathVariable Long auxiliaryReposi } @Override - @GetMapping(value = "auxiliary-repository/{auxiliaryRepositoryId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "{auxiliaryRepositoryId}", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastTutor public ResponseEntity getStatus(@PathVariable Long auxiliaryRepositoryId) throws GitAPIException { return super.getStatus(auxiliaryRepositoryId); @@ -187,16 +187,16 @@ public ResponseEntity getStatus(@PathVariable Long auxiliar * @param principal used to check if the user can update the files * @return {Map} file submissions or the appropriate http error */ - @PutMapping("auxiliary-repository/{auxiliaryRepositoryId}/files") + @PutMapping("{auxiliaryRepositoryId}/files") @EnforceAtLeastTutor - public ResponseEntity> updateTestFiles(@PathVariable("auxiliaryRepositoryId") Long auxiliaryRepositoryId, @RequestBody List submissions, - @RequestParam Boolean commit, Principal principal) { + public ResponseEntity> updateAuxiliaryFiles(@PathVariable("auxiliaryRepositoryId") Long auxiliaryRepositoryId, + @RequestBody List submissions, @RequestParam Boolean commit, Principal principal) { if (versionControlService.isEmpty()) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "VCSNotPresent"); } AuxiliaryRepository auxiliaryRepository = auxiliaryRepositoryRepository.findByIdElseThrow(auxiliaryRepositoryId); - ProgrammingExercise exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(auxiliaryRepository.getExercise().getId()); + ProgrammingExercise exercise = auxiliaryRepository.getExercise(); Repository repository; try { diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index 5b437f9f2a6b..b44b87386158 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -192,9 +192,9 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer * @param repositoryType the repositories type * @param auxiliaryRepositoryId the id of the repository */ - retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, repositoryType: string, auxiliaryRepositoryId: number): Observable { + retrieveCommitHistoryForAuxiliaryRepository(exerciseId: number, auxiliaryRepositoryId: number): Observable { const params: { [key: string]: number } = {}; params['repositoryId'] = auxiliaryRepositoryId; - return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/${repositoryType}`, { params: params }); + return this.http.get(`${this.resourceUrl}${exerciseId}/commit-history/AUXILIARY`, { params: params }); } } diff --git a/src/main/webapp/app/localvc/commit-history/commit-history.component.ts b/src/main/webapp/app/localvc/commit-history/commit-history.component.ts index 22b513cb6530..f734f9f2eec3 100644 --- a/src/main/webapp/app/localvc/commit-history/commit-history.component.ts +++ b/src/main/webapp/app/localvc/commit-history/commit-history.component.ts @@ -158,7 +158,7 @@ export class CommitHistoryComponent implements OnInit, OnDestroy { */ private handleAuxiliaryRepositoryCommits() { this.commitsInfoSubscription = this.programmingExerciseParticipationService - .retrieveCommitHistoryForAuxiliaryRepository(this.exerciseId, this.repositoryType, this.repositoryId!) + .retrieveCommitHistoryForAuxiliaryRepository(this.exerciseId, this.repositoryId!) .subscribe((commits) => { this.commits = this.sortCommitsByTimestampDesc(commits); }); From 448a9c41d2212c669a0364eee0bbabe3dc9c6a51 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 20:47:25 +0200 Subject: [PATCH 11/20] adjusted mock --- .../service/mock-programming-exercise-participation.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts index 00ecf361d725..3bca2029aa5c 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise-participation.service.ts @@ -11,7 +11,7 @@ export class MockProgrammingExerciseParticipationService implements IProgramming getStudentParticipationWithAllResults = (participationId: number) => of({} as ProgrammingExerciseStudentParticipation); retrieveCommitHistoryForParticipation = (participationId: number) => of([] as CommitInfo[]); retrieveCommitHistoryForTemplateSolutionOrTests = (participationId: number, repositoryType: string) => of([] as CommitInfo[]); - retrieveCommitHistoryForAuxiliaryRepository = (exerciseId: number, repositoryType: string, repositoryId: number) => of([] as CommitInfo[]); + retrieveCommitHistoryForAuxiliaryRepository = (exerciseId: number, repositoryId: number) => of([] as CommitInfo[]); getParticipationRepositoryFilesWithContentAtCommitForCommitDetailsView = (exerciseId: number, participationId: number, commitId: string, repositoryType: string) => of(new Map()); checkIfParticipationHasResult = (participationId: number) => of(true); From 0981cc7c2c106e048d051b9e8d77d78e54287b87 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 20:53:04 +0200 Subject: [PATCH 12/20] renamed and adjusted comment --- .../web/ProgrammingExerciseParticipationResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index 5fa96dc0b30a..344a17737147 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -343,7 +343,7 @@ public ResponseEntity> getVcsAccessLogForParticipationRepo /** * GET /programming-exercise/{exerciseID}/commit-history/{repositoryType} : Get the commit history of a programming exercise repository. The repository type can be TEMPLATE or - * SOLUTION or TESTS. + * SOLUTION, TESTS or AUXILIARY. * Here we check is at least a teaching assistant for the exercise. * * @param exerciseID the id of the exercise for which to retrieve the commit history @@ -353,7 +353,7 @@ public ResponseEntity> getVcsAccessLogForParticipationRepo */ @GetMapping("programming-exercise/{exerciseID}/commit-history/{repositoryType}") @EnforceAtLeastTutor - public ResponseEntity> getCommitHistoryForTemplateSolutionOrTestRepo(@PathVariable long exerciseID, @PathVariable RepositoryType repositoryType, + public ResponseEntity> getCommitHistoryForTemplateSolutionTestOrAuxRepo(@PathVariable long exerciseID, @PathVariable RepositoryType repositoryType, @RequestParam long repositoryId) { boolean isTemplateRepository = repositoryType.equals(RepositoryType.TEMPLATE); boolean isSolutionRepository = repositoryType.equals(RepositoryType.SOLUTION); From 697b3184ffb233cdd480313fb0083713acaec82c Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Sep 2024 21:52:45 +0200 Subject: [PATCH 13/20] fix tests --- ...AuxiliaryRepositoryResourceIntegrationTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java index 184cbd1991ec..84cf56aca4c6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java @@ -178,7 +178,7 @@ void testCreateFile_alreadyExists() throws Exception { params.add("file", "newFile"); doReturn(Optional.of(true)).when(gitService).getFileByName(any(), any()); - request.postWithoutResponseBody(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.BAD_REQUEST, params); + request.postWithoutResponseBody(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.BAD_REQUEST, params); } @Test @@ -288,7 +288,7 @@ void testDeleteFile_notFound() throws Exception { doReturn(Optional.empty()).when(gitService).getFileByName(any(), any()); - request.delete(testRepoBaseUrl + programmingExercise.getId() + "/file", HttpStatus.NOT_FOUND, params); + request.delete(testRepoBaseUrl + auxiliaryRepository.getId() + "/file", HttpStatus.NOT_FOUND, params); } @Test @@ -420,7 +420,7 @@ void testPullChanges() throws Exception { assertThat(localAuxiliaryRepo.getAllLocalCommits().getFirst()).isNotEqualTo(localAuxiliaryRepo.getAllOriginCommits().getFirst()); // Execute the Rest call - request.get(testRepoBaseUrl + programmingExercise.getId() + "/pull", HttpStatus.OK, Void.class); + request.get(testRepoBaseUrl + auxiliaryRepository.getId() + "/pull", HttpStatus.OK, Void.class); // Check if the current commit is the same on the local and the remote repository and if the file exists on the local repository assertThat(localAuxiliaryRepo.getAllLocalCommits().getFirst()).isEqualTo(localAuxiliaryRepo.getAllOriginCommits().getFirst()); @@ -437,14 +437,14 @@ void testResetToLastCommit() throws Exception { var remoteRepo = gitService.getExistingCheckedOutRepositoryByLocalPath(localAuxiliaryRepo.originRepoFile.toPath(), null)) { // Check status of git before the commit - var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusBeforeCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusBeforeCommit.repositoryStatus()).hasToString("UNCOMMITTED_CHANGES"); // Create a commit for the local and the remote repository - request.postWithoutLocation(testRepoBaseUrl + programmingExercise.getId() + "/commit", null, HttpStatus.OK, null); + request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/commit", null, HttpStatus.OK, null); // Check status of git after the commit - var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); // Create file in the local repository and commit it @@ -496,7 +496,7 @@ void testGetStatus() throws Exception { request.postWithoutLocation(testRepoBaseUrl + auxiliaryRepository.getId() + "/commit", null, HttpStatus.OK, null); // Check if the status of git is "clean" after the commit - var receivedStatusAfterCommit = request.get(testRepoBaseUrl + programmingExercise.getId(), HttpStatus.OK, RepositoryStatusDTO.class); + var receivedStatusAfterCommit = request.get(testRepoBaseUrl + auxiliaryRepository.getId(), HttpStatus.OK, RepositoryStatusDTO.class); assertThat(receivedStatusAfterCommit.repositoryStatus()).hasToString("CLEAN"); } From 4acc6a7abc298a37cec395df92489244252aa151 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 26 Sep 2024 11:23:46 +0200 Subject: [PATCH 14/20] simplified endpoint --- .../web/ProgrammingExerciseResource.java | 13 ++++--------- .../manage/services/programming-exercise.service.ts | 5 ++--- .../repository-view/repository-view.component.ts | 3 +-- .../service/mock-programming-exercise.service.ts | 2 +- .../service/programming-exercise.service.spec.ts | 2 +- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 67dd289da73c..aad4fef60d51 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -532,22 +532,17 @@ public ResponseEntity getProgrammingExerciseWithTemplateAnd } /** - * GET /programming-exercises/:exerciseId/with-auxiliary-repository/:auxiliaryRepositoryId + * GET /programming-exercises/:exerciseId/with-auxiliary-repository * - * @param exerciseId the id of the programmingExercise to retrieve - * @param auxiliaryRepositoryId the id of the auxiliary repository of the exercise + * @param exerciseId the id of the programmingExercise to retrieve * @return the ResponseEntity with status 200 (OK) and the programming exercise with template and solution participation, or with status 404 (Not Found) */ - @GetMapping("programming-exercises/{exerciseId}/with-auxiliary-repository/{auxiliaryRepositoryId}") + @GetMapping("programming-exercises/{exerciseId}/with-auxiliary-repository") @EnforceAtLeastTutorInExercise - public ResponseEntity getProgrammingExerciseWithAuxiliaryRepository(@PathVariable long exerciseId, @PathVariable long auxiliaryRepositoryId) { + public ResponseEntity getProgrammingExerciseWithAuxiliaryRepository(@PathVariable long exerciseId) { log.debug("REST request to get programming exercise with auxiliary repositories: {}", exerciseId); final var programmingExercise = programmingExerciseService.loadProgrammingExerciseWithAuxiliaryRepositories(exerciseId); - if (programmingExercise.getAuxiliaryRepositories().stream().noneMatch(id -> id.getId() == auxiliaryRepositoryId)) { - throw new BadRequestAlertException("The auxiliary repository Id does not belong to the exercise.", "Exercise", - ProgrammingExerciseResourceErrorKeys.INVALID_AUXILIARY_REPOSITORY_ID); - } return ResponseEntity.ok(programmingExercise); } diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts index c20975d6e2bc..5b1a793288ff 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts @@ -286,10 +286,9 @@ export class ProgrammingExerciseService { /** * Finds the programming exercise for the given exerciseId with its auxiliary repositories * @param programmingExerciseId of the programming exercise to retrieve - * @param auxiliaryRepositoryId of the auxiliary repository */ - findWithAuxiliaryRepository(programmingExerciseId: number, auxiliaryRepositoryId: number): Observable { - return this.http.get(`${this.resourceUrl}/${programmingExerciseId}/with-auxiliary-repository/${auxiliaryRepositoryId}`, { + findWithAuxiliaryRepository(programmingExerciseId: number): Observable { + return this.http.get(`${this.resourceUrl}/${programmingExerciseId}/with-auxiliary-repository`, { observe: 'response', }); } diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts index 04c1679eb87e..3a5aa026532c 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts @@ -197,7 +197,7 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { private loadAuxiliaryRepository(exerciseId: number, auxiliaryRepositoryId: number) { this.programmingExerciseService - .findWithAuxiliaryRepository(exerciseId, auxiliaryRepositoryId) + .findWithAuxiliaryRepository(exerciseId) .pipe( tap((exerciseResponse) => { this.exercise = exerciseResponse.body!; @@ -214,7 +214,6 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { }, error: () => { this.participationCouldNotBeFetched = true; - this.loadingParticipation = false; }, }); } diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts index 5d320caff6c8..27c111ba7665 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts @@ -9,7 +9,7 @@ export class MockProgrammingExerciseService { findWithTemplateAndSolutionParticipation = (exerciseId: number) => of(); findWithTemplateAndSolutionParticipationAndResults = (exerciseId: number) => of(); findWithTemplateAndSolutionParticipationAndLatestResults = (exerciseId: number) => of(); - findWithAuxiliaryRepository = (programmingExerciseId: number, auxiliaryRepositoryId: number) => of(); + findWithAuxiliaryRepository = (programmingExerciseId: number) => of(); find = (exerciseId: number) => of({ body: { id: 4 } }); getProgrammingExerciseTestCaseState = (exerciseId: number) => of({ body: { released: true, hasStudentResult: true, testCasesChanged: false } }); exportInstructorExercise = (exerciseId: number) => of({ body: undefined }); diff --git a/src/test/javascript/spec/service/programming-exercise.service.spec.ts b/src/test/javascript/spec/service/programming-exercise.service.spec.ts index 4e298e77f324..6a3d4ce26987 100644 --- a/src/test/javascript/spec/service/programming-exercise.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise.service.spec.ts @@ -100,7 +100,7 @@ describe('ProgrammingExercise Service', () => { }; const expected = { ...returnedFromService }; service - .findWithAuxiliaryRepository(returnedFromService.id!, auxiliaryRepository.id!) + .findWithAuxiliaryRepository(returnedFromService.id!) .pipe(take(1)) .subscribe((resp) => expect(resp.body).toEqual(expected)); const req = httpMock.expectOne({ method: 'GET' }); From 53480bbf5257b95f6c1c9467d9ee3d5628ac22ed Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 26 Sep 2024 13:15:31 +0200 Subject: [PATCH 15/20] fixed routing --- ...y-repository-buttons-detail.component.html | 2 +- ...ming-exercise-management-routing.module.ts | 20 +++++++++++++++---- .../repository-view.component.ts | 4 ++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html index 374b8d3a9c5b..26cf22829413 100644 --- a/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html +++ b/src/main/webapp/app/detail-overview-list/components/programming-auxiliary-repository-buttons-detail/programming-auxiliary-repository-buttons-detail.component.html @@ -7,7 +7,7 @@ class="ms-2" [smallButtons]="true" [repositoryUri]="auxiliaryRepository.repositoryUri" - [routerLinkForRepositoryView]="['.', 'repository', 'AUXILIARY', auxiliaryRepository.id]" + [routerLinkForRepositoryView]="['.', 'repository', 'AUXILIARY', 'repo', auxiliaryRepository.id]" /> Date: Thu, 26 Sep 2024 14:04:53 +0200 Subject: [PATCH 16/20] fixed rest call by making repositoryId request param optional --- .../web/ProgrammingExerciseParticipationResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java index 344a17737147..be1c99c67be6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseParticipationResource.java @@ -354,7 +354,7 @@ public ResponseEntity> getVcsAccessLogForParticipationRepo @GetMapping("programming-exercise/{exerciseID}/commit-history/{repositoryType}") @EnforceAtLeastTutor public ResponseEntity> getCommitHistoryForTemplateSolutionTestOrAuxRepo(@PathVariable long exerciseID, @PathVariable RepositoryType repositoryType, - @RequestParam long repositoryId) { + @RequestParam Optional repositoryId) { boolean isTemplateRepository = repositoryType.equals(RepositoryType.TEMPLATE); boolean isSolutionRepository = repositoryType.equals(RepositoryType.SOLUTION); boolean isTestRepository = repositoryType.equals(RepositoryType.TESTS); @@ -373,7 +373,7 @@ else if (isTemplateRepository) { participationAuthCheckService.checkCanAccessParticipationElseThrow(participation); if (isAuxiliaryRepository) { - var auxiliaryRepo = auxiliaryRepositoryRepository.findByIdElseThrow(repositoryId); + var auxiliaryRepo = auxiliaryRepositoryRepository.findByIdElseThrow(repositoryId.orElseThrow()); if (!auxiliaryRepo.getExercise().getId().equals(exerciseID)) { throw new BadRequestAlertException("Invalid repository id", ENTITY_NAME, "invalidRepositoryId"); } From 160677c4d8fa1a9b6020023df83445946d12f141 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 26 Sep 2024 15:51:28 +0200 Subject: [PATCH 17/20] fix test --- .../spec/component/localvc/repository-view.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts index 3924968f0f0d..512e1abae71a 100644 --- a/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts +++ b/src/test/javascript/spec/component/localvc/repository-view.component.spec.ts @@ -185,7 +185,7 @@ describe('RepositoryViewComponent', () => { const exerciseId = 1; const auxiliaryRepositoryId = 5; - activatedRoute.setParameters({ exerciseId: exerciseId, repositoryType: 'AUXILIARY', auxiliaryRepositoryId: auxiliaryRepositoryId }); + activatedRoute.setParameters({ exerciseId: exerciseId, repositoryType: 'AUXILIARY', repositoryId: auxiliaryRepositoryId }); jest.spyOn(programmingExerciseService, 'findWithAuxiliaryRepository').mockReturnValue(of(mockExerciseResponse)); // Trigger ngOnInit From bd114903f8b212e4288b4c7fa08c5dd3b91a646a Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 26 Sep 2024 16:21:59 +0200 Subject: [PATCH 18/20] remove unused attribute --- .../app/localvc/repository-view/repository-view.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts index 123f5a7670fb..8e0b537d7abd 100644 --- a/src/main/webapp/app/localvc/repository-view/repository-view.component.ts +++ b/src/main/webapp/app/localvc/repository-view/repository-view.component.ts @@ -47,7 +47,6 @@ export class RepositoryViewComponent implements OnInit, OnDestroy { repositoryType: ProgrammingExerciseInstructorRepositoryType | 'USER'; enableVcsAccessLog = false; allowVcsAccessLog = false; - auxiliaryRepositoryId: number; result: Result; resultHasInlineFeedback = false; showInlineFeedback = false; From 9c223976113dcef658dae2dca63203bb06b31d0f Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 30 Sep 2024 23:52:59 +0200 Subject: [PATCH 19/20] fix imports --- .../AuxiliaryRepositoryResourceIntegrationTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java index 84cf56aca4c6..eff890c612de 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java @@ -31,7 +31,6 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.cit.aet.artemis.AbstractSpringIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.File; @@ -44,9 +43,11 @@ import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.service.GitService; +import de.tum.cit.aet.artemis.programming.util.GitUtilService; +import de.tum.cit.aet.artemis.programming.util.LocalRepository; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; import de.tum.cit.aet.artemis.programming.web.repository.FileSubmission; -import de.tum.cit.aet.artemis.util.GitUtilService; -import de.tum.cit.aet.artemis.util.LocalRepository; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; class AuxiliaryRepositoryResourceIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { From 85a5de348559ec7bb7442895b0f1a6529546674e Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 1 Oct 2024 23:33:06 +0200 Subject: [PATCH 20/20] use testRepository --- .../AuxiliaryRepositoryResourceIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java index eff890c612de..4a4970190b88 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/programming/AuxiliaryRepositoryResourceIntegrationTest.java @@ -41,8 +41,8 @@ import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.service.GitService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.GitUtilService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; @@ -54,7 +54,7 @@ class AuxiliaryRepositoryResourceIntegrationTest extends AbstractSpringIntegrati private static final String TEST_PREFIX = "auxiliaryrepositoryresourceint"; @Autowired - private ProgrammingExerciseRepository programmingExerciseRepository; + private ProgrammingExerciseTestRepository programmingExerciseRepository; @Autowired private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository;