Skip to content

Commit

Permalink
feat(GGs): implement moving and renaming of files and directories (#896)
Browse files Browse the repository at this point in the history
* Rename some gitHubService to repoService for clarity

* Implement renameSinglePath and moveFiles

* Adjust BaseDirectoryService and MediaFileService

* Adjust BaseDirectoryService and MediaFileService unit tests

* Use git mv instead to handle case sensitivity

* Fix error types

* Specify correct path if oldPath is an empty string

* Add unit tests for GitFileSystemService

* Add unit tests for RepoService

* Adjust logging message

* Ensure that oldPath is a directory

* Adjust names and unit tests after integration
  • Loading branch information
dcshzj authored Aug 15, 2023
1 parent 0b007dc commit 031cb24
Show file tree
Hide file tree
Showing 15 changed files with 1,210 additions and 460 deletions.
2 changes: 1 addition & 1 deletion src/integration/NotificationOnEditHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const collectionYmlService = new CollectionYmlService({
gitHubService: mockGithubService,
})
const baseDirectoryService = new BaseDirectoryService({
gitHubService: mockGithubService,
repoService: mockGithubService,
})

const contactUsService = new ContactUsPageService({
Expand Down
4 changes: 3 additions & 1 deletion src/integration/Notifications.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ const usersService = getUsersService(sequelize)
const configYmlService = new ConfigYmlService({ gitHubService })
const footerYmlService = new FooterYmlService({ gitHubService })
const collectionYmlService = new CollectionYmlService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})

const contactUsService = new ContactUsPageService({
gitHubService,
Expand Down
4 changes: 3 additions & 1 deletion src/integration/Privatisation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ const usersService = getUsersService(sequelize)
const isomerAdminsService = new IsomerAdminsService({ repository: IsomerAdmin })
const footerYmlService = new FooterYmlService({ gitHubService })
const collectionYmlService = new CollectionYmlService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})

const contactUsService = new ContactUsPageService({
gitHubService,
Expand Down
4 changes: 3 additions & 1 deletion src/integration/Reviews.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ const usersService = getUsersService(sequelize)
const isomerAdminsService = new IsomerAdminsService({ repository: IsomerAdmin })
const footerYmlService = new FooterYmlService({ gitHubService })
const collectionYmlService = new CollectionYmlService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})

const contactUsService = new ContactUsPageService({
gitHubService,
Expand Down
4 changes: 3 additions & 1 deletion src/integration/Sites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ const contactUsService = new ContactUsPageService({
gitHubService,
footerYmlService,
})
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})
const resourcePageService = new ResourcePageService({ gitHubService })
const resourceRoomDirectoryService = new ResourceRoomDirectoryService({
baseDirectoryService,
Expand Down
6 changes: 4 additions & 2 deletions src/routes/v2/authenticatedSites/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ const getAuthenticatedSitesSubrouter = ({
})
const unlinkedPageService = new UnlinkedPageService({ gitHubService })
const resourcePageService = new ResourcePageService({ gitHubService })
const mediaFileService = new MediaFileService({ gitHubService })
const mediaFileService = new MediaFileService({ repoService: gitHubService })
const moverService = new MoverService({
unlinkedPageService,
collectionPageService,
subcollectionPageService,
})
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})
const unlinkedPagesDirectoryService = new UnlinkedPagesDirectoryService({
baseDirectoryService,
moverService,
Expand Down
4 changes: 3 additions & 1 deletion src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ const gitHubService = new RepoService(
const configYmlService = new ConfigYmlService({ gitHubService })
const footerYmlService = new FooterYmlService({ gitHubService })
const collectionYmlService = new CollectionYmlService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({ gitHubService })
const baseDirectoryService = new BaseDirectoryService({
repoService: gitHubService,
})

const contactUsService = new ContactUsPageService({
gitHubService,
Expand Down
210 changes: 194 additions & 16 deletions src/services/db/GitFileSystemService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ import type {
import type { IsomerCommitMessage } from "@root/types/github"
import { ALLOWED_FILE_EXTENSIONS } from "@root/utils/file-upload-utils"

/**
* Some notes:
* - Seems like getTree, updateTree and updateRepoState is always used together
*/

const EFS_VOL_PATH = config.get("aws.efs.volPath")
const BRANCH_REF = config.get("github.branchRef")

Expand All @@ -49,6 +44,19 @@ export default class GitFileSystemService {
this.git = git
}

isDefaultLogFields(logFields: unknown): logFields is DefaultLogFields {
const c = logFields as DefaultLogFields
return (
!!logFields &&
typeof logFields === "object" &&
typeof c.author_name === "string" &&
typeof c.author_email === "string" &&
typeof c.date === "string" &&
typeof c.message === "string" &&
typeof c.hash === "string"
)
}

isGitInitialized(repoName: string): ResultAsync<boolean, GitFileSystemError> {
return ResultAsync.fromPromise(
this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).checkIsRepo(),
Expand Down Expand Up @@ -871,17 +879,187 @@ export default class GitFileSystemService {
})
}

isDefaultLogFields(logFields: unknown): logFields is DefaultLogFields {
const c = logFields as DefaultLogFields
return (
!!logFields &&
typeof logFields === "object" &&
typeof c.author_name === "string" &&
typeof c.author_email === "string" &&
typeof c.date === "string" &&
typeof c.message === "string" &&
typeof c.hash === "string"
)
// Rename a single file or directory
renameSinglePath(
repoName: string,
oldPath: string,
newPath: string,
userId: string,
message?: string
): ResultAsync<string, GitFileSystemError | ConflictError | NotFoundError> {
let oldStateSha = ""

return this.getLatestCommitOfBranch(repoName, BRANCH_REF)
.andThen((latestCommit) => {
// It is guaranteed that the latest commit contains the SHA hash
oldStateSha = latestCommit.sha as string
return okAsync(true)
})
.andThen(() => this.getFilePathStats(repoName, oldPath))
.andThen(() =>
// We expect to see an error here, since the new path should not exist
this.getFilePathStats(repoName, newPath)
.andThen(() =>
errAsync(new ConflictError("File path already exists"))
)
.map(() => true)
.orElse((error) => {
if (error instanceof NotFoundError) {
return okAsync(true)
}

return errAsync(error)
})
)
.andThen(() =>
ResultAsync.fromPromise(
this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).mv(oldPath, newPath),
(error) => {
logger.error(`Error when moving ${oldPath} to ${newPath}: ${error}`)

if (error instanceof GitError) {
return new GitFileSystemNeedsRollbackError(
`Unable to rename ${oldPath} to ${newPath}`
)
}

return new GitFileSystemNeedsRollbackError(
"An unknown error occurred"
)
}
)
)
.andThen(() =>
this.commit(
repoName,
[oldPath, newPath],
userId,
message || `Renamed ${oldPath} to ${newPath}`
)
)
.orElse((error) => {
if (error instanceof GitFileSystemNeedsRollbackError) {
return this.rollback(repoName, oldStateSha).andThen(() =>
errAsync(new GitFileSystemError(error.message))
)
}

return errAsync(error)
})
}

// Move multiple files from oldPath to newPath without renaming them
moveFiles(
repoName: string,
oldPath: string,
newPath: string,
userId: string,
targetFiles: string[],
message?: string
): ResultAsync<string, GitFileSystemError | ConflictError | NotFoundError> {
let oldStateSha = ""

return this.getLatestCommitOfBranch(repoName, BRANCH_REF)
.andThen((latestCommit) => {
// It is guaranteed that the latest commit contains the SHA hash
oldStateSha = latestCommit.sha as string
return okAsync(true)
})
.andThen(() => this.getFilePathStats(repoName, oldPath))
.andThen((stats) => {
if (!stats.isDirectory()) {
return errAsync(
new GitFileSystemError(
`Path "${oldPath}" is not a valid directory in repo "${repoName}"`
)
)
}
return okAsync(true)
})
.andThen(() =>
// Ensure that the new path exists
ResultAsync.fromPromise(
fs.promises.mkdir(`${EFS_VOL_PATH}/${repoName}/${newPath}`, {
recursive: true,
}),
(error) => {
logger.error(`Error when creating ${newPath} during move: ${error}`)

if (error instanceof Error) {
return new GitFileSystemNeedsRollbackError(
`Unable to create ${newPath}`
)
}

return new GitFileSystemNeedsRollbackError(
"An unknown error occurred"
)
}
)
)
.andThen(() =>
combine(
targetFiles.map((targetFile) =>
// We expect to see an error here, since the new path should not exist
this.getFilePathStats(repoName, `${newPath}/${targetFile}`)
.andThen(() =>
errAsync(new ConflictError("File path already exists"))
)
.map(() => true)
.orElse((error) => {
if (error instanceof NotFoundError) {
return okAsync(true)
}

return errAsync(error)
})
.andThen(() => {
const oldFilePath =
oldPath === "" ? targetFile : `${oldPath}/${targetFile}`
const newFilePath =
newPath === "" ? targetFile : `${newPath}/${targetFile}`

return ResultAsync.fromPromise(
this.git
.cwd(`${EFS_VOL_PATH}/${repoName}`)
.mv(`${oldFilePath}`, `${newFilePath}`),
(error) => {
logger.error(
`Error when moving ${targetFile} in ${oldPath} to ${newPath}: ${error}`
)

if (error instanceof GitError) {
return new GitFileSystemNeedsRollbackError(
`Unable to move ${targetFile} to ${newPath}`
)
}

return new GitFileSystemNeedsRollbackError(
"An unknown error occurred"
)
}
)
})
)
)
)
.andThen(() =>
this.commit(
repoName,
[oldPath, newPath],
userId,
message || `Moved selected files from ${oldPath} to ${newPath}`
)
)
.orElse((error) => {
if (error instanceof GitFileSystemNeedsRollbackError) {
return this.rollback(repoName, oldStateSha).andThen(() =>
errAsync(new GitFileSystemError(error.message))
)
}

return errAsync(error)
})
}

getLatestCommitOfBranch(
Expand Down
Loading

0 comments on commit 031cb24

Please sign in to comment.