diff --git a/app/Http/Controllers/BuildController.php b/app/Http/Controllers/BuildController.php index ff106927ff..9a56c2a618 100644 --- a/app/Http/Controllers/BuildController.php +++ b/app/Http/Controllers/BuildController.php @@ -116,7 +116,10 @@ public function update(int $build_id): View { $this->setBuildById($build_id); - return $this->vue('build-update', 'Files Updated'); + return $this->vue('build-update', 'Files Updated', [ + 'repository-type' => $this->project->CvsViewerType, + 'repository-url' => $this->project->CvsUrl, + ]); } public function tests(int $build_id): View diff --git a/resources/js/vue/components/BuildUpdate.vue b/resources/js/vue/components/BuildUpdate.vue index 4656b66d51..049cbac769 100644 --- a/resources/js/vue/components/BuildUpdate.vue +++ b/resources/js/vue/components/BuildUpdate.vue @@ -15,7 +15,7 @@ {{ cdash.update.revision }} @@ -27,7 +27,7 @@ {{ cdash.update.priorrevision }} @@ -131,11 +131,24 @@ import LoadingIndicator from './shared/LoadingIndicator.vue'; import CodeBox from './shared/CodeBox.vue'; import {faChevronDown, faChevronRight} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; +import {getRepository} from './shared/RepositoryIntegrations'; export default { name: 'BuildUpdate', components: {FontAwesomeIcon, CodeBox, LoadingIndicator, BuildSummaryCard}, + props: { + repositoryType: { + type: String, + required: true, + }, + + repositoryUrl: { + type: String, + required: true, + }, + }, + data() { return { // API results. @@ -167,6 +180,10 @@ export default { faChevronRight, }; }, + + repository() { + return getRepository(this.repositoryType, this.repositoryUrl); + }, }, async mounted() { diff --git a/resources/js/vue/components/shared/RepositoryIntegrations.js b/resources/js/vue/components/shared/RepositoryIntegrations.js new file mode 100644 index 0000000000..1f685df0ac --- /dev/null +++ b/resources/js/vue/components/shared/RepositoryIntegrations.js @@ -0,0 +1,83 @@ +/** + * Combine URL components, ensuring that trailing slashes are stripped before concatenation. + * + * @param {...String} components + */ +function makeUrlFromComponents(...components) { + return components.map(part => part.replace(/\/+$/, '')).join('/'); +} + +export class Repository { + constructor(repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + /** + * @param {String} commit + * @return String + */ + getCommitUrl(commit) { // eslint-disable-line no-unused-vars + throw new Error('Method not implemented for abstract Repository class.'); + } + + /** + * @param {String} commit1 + * @param {String} commit2 + * @return String + */ + getComparisonUrl(commit1, commit2) { // eslint-disable-line no-unused-vars + throw new Error('Method not implemented for abstract Repository class.'); + } + + /** + * @param {String} commit + * @param {String} path + * @return String + */ + getFileUrl(commit, path) { // eslint-disable-line no-unused-vars + throw new Error('Method not implemented for abstract Repository class.'); + } +} + +export class GitHub extends Repository { + getCommitUrl(commit) { + return makeUrlFromComponents(this.repositoryUrl, 'commit', commit); + } + + getComparisonUrl(commit1, commit2) { + return makeUrlFromComponents(this.repositoryUrl, 'compare', `${commit1}...${commit2}`); + } + + getFileUrl(commit, path) { + return makeUrlFromComponents(this.repositoryUrl, 'blob', commit, path); + } +} + +export class GitLab extends Repository { + getCommitUrl(commit) { + return makeUrlFromComponents(this.repositoryUrl, '-', 'commit', commit); + } + + getComparisonUrl(commit1, commit2) { + return makeUrlFromComponents(this.repositoryUrl, '-', 'compare', `${commit1}...${commit2}`); + } + getFileUrl(commit, path) { + return makeUrlFromComponents(this.repositoryUrl, '-', 'blob', commit, path); + } +} + +/** + * @param {String} repositoryType + * @param {String} repositoryUrl + * @return ?Repository + */ +export function getRepository(repositoryType, repositoryUrl) { + switch (repositoryType.toLowerCase()) { + case 'github': + return new GitHub(repositoryUrl); + case 'gitlab': + return new GitLab(repositoryUrl); + default: + return null; + } +} diff --git a/tests/Spec/CMakeLists.txt b/tests/Spec/CMakeLists.txt index d8b3254bfa..a6b2cdc3db 100644 --- a/tests/Spec/CMakeLists.txt +++ b/tests/Spec/CMakeLists.txt @@ -1,4 +1,4 @@ -function(add_vue_test TestName) +function(add_jest_test TestName) add_test( NAME "Spec/${TestName}" COMMAND "node_modules/.bin/jest" "tests/Spec/${TestName}.spec.js" @@ -6,7 +6,8 @@ function(add_vue_test TestName) ) endfunction() -add_vue_test(build-summary) -add_vue_test(edit-project) -add_vue_test(manage-measurements) -add_vue_test(test-details) +add_jest_test(build-summary) +add_jest_test(edit-project) +add_jest_test(manage-measurements) +add_jest_test(test-details) +add_jest_test(repository-integrations) diff --git a/tests/Spec/repository-integrations.spec.js b/tests/Spec/repository-integrations.spec.js new file mode 100644 index 0000000000..2d29883d94 --- /dev/null +++ b/tests/Spec/repository-integrations.spec.js @@ -0,0 +1,104 @@ +import { + getRepository, + GitHub, + GitLab, + Repository, +} from '../../resources/js/vue/components/shared/RepositoryIntegrations'; + +describe('RepositoryIntegrations', () => { + describe('getRepository', () => { + it('returns a GitHub instance for "github" type', () => { + const repo = getRepository('github', 'https://github.com/foo/bar'); + expect(repo).toBeInstanceOf(GitHub); + expect(repo.repositoryUrl).toBe('https://github.com/foo/bar'); + }); + + it('returns a GitLab instance for "gitlab" type', () => { + const repo = getRepository('gitlab', 'https://gitlab.com/foo/bar'); + expect(repo).toBeInstanceOf(GitLab); + expect(repo.repositoryUrl).toBe('https://gitlab.com/foo/bar'); + }); + + it('is case insensitive for repository type', () => { + const repo = getRepository('GitHub', 'https://github.com/foo/bar'); + expect(repo).toBeInstanceOf(GitHub); + }); + + it('returns null for unknown repository types', () => { + const repo = getRepository('bitbucket', 'https://bitbucket.org/foo/bar'); + expect(repo).toBeNull(); + }); + }); + + describe('GitHub', () => { + const repoUrl = 'https://github.com/foo/bar'; + const repo = new GitHub(repoUrl); + + it('generates correct commit URL', () => { + const commit = 'abcdef123456'; + expect(repo.getCommitUrl(commit)).toBe(`${repoUrl}/commit/${commit}`); + }); + + it('generates correct comparison URL', () => { + const commit1 = 'abc'; + const commit2 = 'def'; + expect(repo.getComparisonUrl(commit1, commit2)).toBe(`${repoUrl}/compare/${commit1}...${commit2}`); + }); + + it('generates correct file URL', () => { + const commit = 'abcdef123456'; + const path = 'src/main.cpp'; + expect(repo.getFileUrl(commit, path)).toBe(`${repoUrl}/blob/${commit}/${path}`); + }); + + it('handles trailing slashes in repository URL', () => { + const repoWithSlash = new GitHub('https://github.com/foo/bar/'); + const commit = '123'; + expect(repoWithSlash.getCommitUrl(commit)).toBe('https://github.com/foo/bar/commit/123'); + }); + }); + + describe('GitLab', () => { + const repoUrl = 'https://gitlab.com/foo/bar'; + const repo = new GitLab(repoUrl); + + it('generates correct commit URL', () => { + const commit = 'abcdef123456'; + expect(repo.getCommitUrl(commit)).toBe(`${repoUrl}/-/commit/${commit}`); + }); + + it('generates correct comparison URL', () => { + const commit1 = 'abc'; + const commit2 = 'def'; + expect(repo.getComparisonUrl(commit1, commit2)).toBe(`${repoUrl}/-/compare/${commit1}...${commit2}`); + }); + + it('generates correct file URL', () => { + const commit = 'abcdef123456'; + const path = 'src/main.cpp'; + expect(repo.getFileUrl(commit, path)).toBe(`${repoUrl}/-/blob/${commit}/${path}`); + }); + + it('handles trailing slashes in repository URL', () => { + const repoWithSlash = new GitLab('https://gitlab.com/foo/bar/'); + const commit = '123'; + expect(repoWithSlash.getCommitUrl(commit)).toBe('https://gitlab.com/foo/bar/-/commit/123'); + }); + }); + + describe('Repository (Abstract)', () => { + const repo = new Repository('https://example.com'); + + it('throws error for unimplemented getCommitUrl', () => { + expect(() => repo.getCommitUrl('123')).toThrow('Method not implemented for abstract Repository class.'); + }); + + it('throws error for unimplemented getComparisonUrl', () => { + expect(() => repo.getComparisonUrl('123', '456')).toThrow('Method not implemented for abstract Repository class.'); + }); + + it('throws error for unimplemented getFileUrl', () => { + expect(() => repo.getFileUrl('123', 'file.txt')).toThrow('Method not implemented for abstract Repository class.'); + }); + }); +});