Skip to content

Commit 6d6c3cd

Browse files
iHiDclaudedem4ron
authored
Auto-sync solution when exercise diff has no files (#8493)
* Auto-sync solution when diff has no files instead of raising error When a solution's git_important_files_hash differs from the exercise's but the actual git diff produces no interesting file changes, auto-sync the solution to the latest exercise version instead of returning a 400 error and reporting to Sentry. The frontend now gracefully handles the empty files case with an auto-updated notice. Closes #8456 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Adjust styling of modal --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: dem4ron <demaaron88@gmail.com>
1 parent 1870ac7 commit 6d6c3cd

5 files changed

Lines changed: 51 additions & 23 deletions

File tree

app/controllers/api/solutions_controller.rb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,7 @@ def unpublish
102102
def diff
103103
files = Git::Exercise::GenerateDiffBetweenVersions.(@solution.exercise, @solution.git_slug, @solution.git_sha)
104104

105-
# TODO: (Optional): Change this to always be a 200 and handle the empty files in React
106-
if files.present?
107-
status = 200
108-
else
109-
status = 400
110-
Sentry.capture_exception(RuntimeError.new("No files were found during solution diff"))
111-
end
105+
Solution::UpdateToLatestExerciseVersion.(@solution) if files.blank?
112106

113107
render json: {
114108
diff: {
@@ -121,7 +115,7 @@ def diff
121115
update: Exercism::Routes.sync_api_solution_url(@solution.uuid)
122116
}
123117
}
124-
}, status:
118+
}, status: :ok
125119
end
126120

127121
def sync

app/css/modals/update-exercise.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
@apply flex flex-col;
66
width: 80%;
77
}
8+
&:has(.auto-updated-notice) .--modal-content {
9+
max-width: 500px;
10+
}
811
& .header {
912
@apply mb-16;
1013
& h2 {

app/javascript/components/modals/ExerciseUpdateModal.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import React from 'react'
44
import { FetchingBoundary } from '../FetchingBoundary'
55
import { Modal, ModalProps } from './Modal'
6-
import { Icon } from '../common'
6+
import { Icon, GraphicalIcon } from '../common'
77
import { useRequestQuery } from '../../hooks/request-query'
88
import { ExerciseUpdateForm } from './exercise-update-modal/ExerciseUpdateForm'
99
import { Exercise } from '../types'
@@ -43,13 +43,47 @@ export const ExerciseUpdateModal = ({
4343
defaultError={DEFAULT_ERROR}
4444
>
4545
{data ? (
46-
<ExerciseUpdateForm diff={data.diff} onCancel={props.onClose} />
46+
data.diff.files.length > 0 ? (
47+
<ExerciseUpdateForm diff={data.diff} onCancel={props.onClose} />
48+
) : (
49+
<AutoUpdatedNotice onClose={props.onClose} />
50+
)
4751
) : null}
4852
</FetchingBoundary>
4953
</Modal>
5054
)
5155
}
5256

57+
const AutoUpdatedNotice = ({ onClose }: { onClose: () => void }) => {
58+
const { t } = useAppTranslation('components/modals/ExerciseUpdateModal.tsx')
59+
return (
60+
<div className="auto-updated-notice flex flex-col items-center text-center">
61+
<GraphicalIcon
62+
icon="completed-check-circle"
63+
height={64}
64+
width={64}
65+
className="mb-20"
66+
/>
67+
<h2 className="text-h3 mb-8">
68+
{t('exerciseUpdateModal.autoUpdatedTitle')}
69+
</h2>
70+
<p className="text-p-large mb-24">
71+
{t('exerciseUpdateModal.autoUpdatedBody')}
72+
</p>
73+
<button
74+
type="button"
75+
className="btn-primary btn-m"
76+
onClick={() => {
77+
onClose()
78+
window.location.reload()
79+
}}
80+
>
81+
{t('exerciseUpdateModal.continue')}
82+
</button>
83+
</div>
84+
)
85+
}
86+
5387
const LoadingComponent = () => {
5488
const { t } = useAppTranslation('components/modals/ExerciseUpdateModal.tsx')
5589
return (
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// namespace: components/modals/ExerciseUpdateModal.tsx
22
export default {
33
'exerciseUpdateModal.loadingExerciseDiff': 'Loading exercise diff',
4-
'exerciseUpdateModal.sorryCantWorkOutWhatNeedsUpdating':
5-
"Sorry - it seems that we can't work out what needs updating for this exercise. We've been alerted and will have a look.",
4+
'exerciseUpdateModal.autoUpdatedTitle': 'Exercise Updated',
5+
'exerciseUpdateModal.autoUpdatedBody':
6+
'This exercise has been automatically updated to the latest version. There are no file changes to review.',
7+
'exerciseUpdateModal.continue': 'Continue',
68
}

test/controllers/api/solutions_controller_test.rb

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,17 @@ class API::SolutionsControllerTest < API::BaseTestCase
173173
assert_equal expected, actual
174174
end
175175

176-
test "Diff returns 400 if diff does not contain files" do
176+
test "Diff auto-syncs and returns 200 when diff does not contain files" do
177177
user = create :user
178178
setup_user(user)
179179
solution = create(:concept_solution, user:)
180-
get diff_api_solution_path(solution.uuid), headers: @headers, as: :json
181-
182-
assert_response :bad_request
183-
end
184-
185-
test "Sentry is alerted if diff does not contain files" do
186-
user = create :user
187-
setup_user(user)
188-
solution = create(:concept_solution, user:)
189-
Sentry.expects(:capture_exception).with(RuntimeError.new("No files were found during solution diff"))
180+
Solution::UpdateToLatestExerciseVersion.expects(:call).with(solution)
190181

191182
get diff_api_solution_path(solution.uuid), headers: @headers, as: :json
183+
184+
assert_response :ok
185+
response_body = JSON.parse(response.body)
186+
assert_empty response_body.dig("diff", "files")
192187
end
193188

194189
########

0 commit comments

Comments
 (0)