Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for recovering deleted files #1269

Merged
merged 10 commits into from
Jun 17, 2023
82 changes: 82 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -601,6 +601,88 @@ jobs:
shell:
bash

test_recover_deleted_file:
name: Test changed-files recover deleted file
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
max-parallel: 4
matrix:
fetch-depth: [0, 1, 2]

steps:
- name: Checkout branch
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive
fetch-depth: ${{ matrix.fetch-depth }}

- name: Download build assets
uses: actions/download-artifact@v3
with:
name: build-assets

- name: Run changed-files with recover_deleted_files
id: changed-files-recover-deleted-files
uses: ./
with:
base_sha: "fcdeb5b3d797752d95f6dbe98552a95c29dad338"
sha: "432e0c810c60ef1332850a971c5ec39022034b4c"
recover_deleted_files: true

- name: Show output
run: |
echo "${{ toJSON(steps.changed-files-recover-deleted-files.outputs) }}"
shell:
bash

- name: Verify deleted files
if: steps.changed-files-recover-deleted-files.outputs.deleted_files != 'test/test deleted.txt'
run: |
echo "Expected: (test/test deleted.txt) got ${{ steps.changed-files-recover-deleted-files.outputs.deleted_files }}"
exit 1

- name: Verify that test/test deleted.txt is restored
run: |
if [ ! -f "test/test deleted.txt" ]; then
echo "Expected: (test/test deleted.txt) to exist"
exit 1
else
cat "test/test deleted.txt"
fi

- name: Run changed-files with recover_deleted_files and recover_deleted_files_to_destination
id: changed-files-recover-deleted-files-to-destination
uses: ./
with:
base_sha: "fcdeb5b3d797752d95f6dbe98552a95c29dad338"
sha: "432e0c810c60ef1332850a971c5ec39022034b4c"
recover_deleted_files: true
recover_deleted_files_to_destination: "deleted_files"

- name: Show output
run: |
echo "${{ toJSON(steps.changed-files-recover-deleted-files-to-destination.outputs) }}"
shell:
bash

- name: Verify deleted files
if: steps.changed-files-recover-deleted-files-to-destination.outputs.deleted_files != 'test/test deleted.txt'
run: |
echo "Expected: (test/test deleted.txt) got ${{ steps.changed-files-recover-deleted-files-to-destination.outputs.deleted_files }}"
exit 1

- name: Verify that test/test deleted.txt is restored
run: |
if [ ! -f "deleted_files/test/test deleted.txt" ]; then
echo "Expected: (deleted_files/test/test deleted.txt) to exist"
exit 1
else
cat "deleted_files/test/test deleted.txt"
fi

test:
name: Test changed-files
runs-on: ${{ matrix.platform }}
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Expand Up @@ -145,6 +145,14 @@ inputs:
description: "Output renamed files as deleted and added files."
required: false
default: "false"
recover_deleted_files:
description: "Recover deleted files."
required: false
default: "false"
recover_deleted_files_to_destination:
description: "Recover deleted files to a new destination directory, defaults to the original location."
required: false
default: ""

outputs:
added_files:
Expand Down
47 changes: 45 additions & 2 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion src/inputs.ts
Expand Up @@ -37,6 +37,8 @@ export type Inputs = {
writeOutputFiles: boolean
outputDir: string
outputRenamedFilesAsDeletedAndAdded: boolean
recoverDeletedFiles: boolean
recoverDeletedFilesToDestination: string
}

export const getInputs = (): Inputs => {
Expand Down Expand Up @@ -145,6 +147,13 @@ export const getInputs = (): Inputs => {
'output_renamed_files_as_deleted_and_added',
{required: false}
)
const recoverDeletedFiles = core.getBooleanInput('recover_deleted_files', {
required: false
})
const recoverDeletedFilesToDestination = core.getInput(
'recover_deleted_files_to_destination',
{required: false}
)

const inputs: Inputs = {
files,
Expand Down Expand Up @@ -180,7 +189,9 @@ export const getInputs = (): Inputs => {
sinceLastRemoteCommit,
writeOutputFiles,
outputDir,
outputRenamedFilesAsDeletedAndAdded
outputRenamedFilesAsDeletedAndAdded,
recoverDeletedFiles,
recoverDeletedFilesToDestination
}

if (fetchDepth) {
Expand Down
10 changes: 9 additions & 1 deletion src/main.ts
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import path from 'path'
import {getAllDiffFiles, getRenamedFiles} from './changedFiles'
import {ChangeTypeEnum, getAllDiffFiles, getRenamedFiles} from './changedFiles'
import {setChangedFilesOutput} from './changedFilesOutput'
import {
DiffResult,
Expand All @@ -14,6 +14,7 @@ import {
getSubmodulePath,
getYamlFilePatterns,
isRepoShallow,
recoverDeletedFiles,
setOutput,
submoduleExists,
updateGitGlobalConfig,
Expand Down Expand Up @@ -118,6 +119,13 @@ export async function run(): Promise<void> {
core.info('All Done!')
core.endGroup()

await recoverDeletedFiles({
inputs,
workingDirectory,
deletedFiles: allDiffFiles[ChangeTypeEnum.Deleted],
sha: diffResult.previousSha
})

const filePatterns = await getFilePatterns({
inputs,
workingDirectory
Expand Down
65 changes: 65 additions & 0 deletions src/utils.ts
Expand Up @@ -1032,3 +1032,68 @@ export const setOutput = async ({
await fs.writeFile(outputFilePath, cleanedValue.replace(/\\"/g, '"'))
}
}

const getDeletedFileContents = async ({
cwd,
filePath,
sha
}: {
cwd: string
filePath: string
sha: string
}): Promise<string> => {
const {stdout, exitCode, stderr} = await exec.getExecOutput(
'git',
['show', `${sha}:${filePath}`],
{
cwd,
silent: process.env.RUNNER_DEBUG !== '1',
ignoreReturnCode: true
}
)

if (exitCode !== 0) {
throw new Error(
`Error getting file content from git history "${filePath}": ${stderr}`
)
}

return stdout
}

export const recoverDeletedFiles = async ({
inputs,
workingDirectory,
deletedFiles,
sha
}: {
inputs: Inputs
workingDirectory: string
deletedFiles: string[]
sha: string
}): Promise<void> => {
if (inputs.recoverDeletedFiles) {
for (const deletedFile of deletedFiles) {
let target = path.join(workingDirectory, deletedFile)

if (inputs.recoverDeletedFilesToDestination) {
target = path.join(
workingDirectory,
inputs.recoverDeletedFilesToDestination,
deletedFile
)
}

const deletedFileContents = await getDeletedFileContents({
cwd: workingDirectory,
filePath: deletedFile,
sha
})

if (!(await exists(path.dirname(target)))) {
await fs.mkdir(path.dirname(target), {recursive: true})
}
await fs.writeFile(target, deletedFileContents)
}
}
}