diff --git a/.github/workflows/check-duplicates.yml b/.github/workflows/check-duplicates.yml new file mode 100644 index 000000000..f2dabf7a6 --- /dev/null +++ b/.github/workflows/check-duplicates.yml @@ -0,0 +1,119 @@ +name: Check duplicated content + +# Reports passages a pull request adds that say the same thing as a passage +# elsewhere under docs/, compared with the merge base, as a summary comment +# plus an inline comment on each added passage. Duplication already on the +# base branch is ignored. Advisory: never fails the PR. + +on: + pull_request: + paths: + - 'docs/**' + - 'dev/check-duplicates.mjs' + - 'dev/sync-review-comments.sh' + - '.github/workflows/check-duplicates.yml' + +# A new push supersedes the run for the previous one +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + check-duplicates: + name: Duplicated content introduced by this PR (advisory) + runs-on: ubuntu-latest + steps: + - name: Check out pull request head + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Check out merge base + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + merge_base=$(git merge-base "$BASE_SHA" HEAD) + git worktree add "$RUNNER_TEMP/base" "$merge_base" + git diff -U0 "$merge_base" HEAD -- docs > "$RUNNER_TEMP/changes.diff" + + # The script exits 1 both with findings and when it crashes. It writes + # its output in one go at the end, so a crash leaves it empty. A crash + # does not fail the check, since it says nothing about the PR; the + # comment step reports it instead of posting the empty report. + - name: Record duplicated passages already present on the base branch + run: | + node dev/check-duplicates.mjs --format json \ + --root "$RUNNER_TEMP/base" > "$RUNNER_TEMP/base-duplicates.json" || true + + - name: Find duplicated passages introduced by this PR + id: check + env: + # File links in the report open the file on the PR branch + LINK_BASE: ${{ github.event.pull_request.head.repo.html_url }}/blob/${{ github.event.pull_request.head.ref }} + run: | + if ! [ -s "$RUNNER_TEMP/base-duplicates.json" ]; then + echo "::warning::check-duplicates crashed on the base branch, so this PR was not checked" + echo "result=crashed" >> "$GITHUB_OUTPUT" + exit 0 + fi + if node dev/check-duplicates.mjs --format markdown \ + --baseline "$RUNNER_TEMP/base-duplicates.json" \ + --diff "$RUNNER_TEMP/changes.diff" \ + --review "$RUNNER_TEMP/review.json" \ + --link-base "$LINK_BASE" > "$RUNNER_TEMP/report.md"; then + echo "result=clean" >> "$GITHUB_OUTPUT" + elif [ -s "$RUNNER_TEMP/report.md" ]; then + echo "result=found" >> "$GITHUB_OUTPUT" + else + echo "::warning::check-duplicates crashed, so this PR was not checked" + echo "result=crashed" >> "$GITHUB_OUTPUT" + fi + cat "$RUNNER_TEMP/report.md" + + - name: Comment on the pull request + # Fork PRs get a read-only token; the report is still in the job log + if: github.event.pull_request.head.repo.full_name == github.repository + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + RESULT: ${{ steps.check.outputs.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + marker='' + existing_comment=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1) + + # Comment when there is something to report, or an earlier report to resolve + case "$RESULT" in + found) + { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; + crashed) + printf '%s\n### ⚠️ The duplicated content check could not run on this revision\n\nThis is a problem with the check, not with this PR; see the [job log](%s).\n' \ + "$marker" "$RUN_URL" > "$RUNNER_TEMP/comment.md" ;; + *) + [ -n "$existing_comment" ] || exit 0 + { echo "$marker"; cat "$RUNNER_TEMP/report.md"; } > "$RUNNER_TEMP/comment.md" ;; + esac + + if [ -n "$existing_comment" ]; then + gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing_comment" \ + --field body=@"$RUNNER_TEMP/comment.md" + else + gh pr comment "$PR_NUMBER" --body-file "$RUNNER_TEMP/comment.md" + fi + + - name: Point out the duplicates as review comments + # One comment per added passage, kept in sync with the findings; + # see dev/sync-review-comments.sh + if: >- + github.event.pull_request.head.repo.full_name == github.repository + && contains(fromJSON('["clean", "found"]'), steps.check.outputs.result) + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: dev/sync-review-comments.sh '`, + onlyEdited + ? 'This passage was already duplicated, and is now out of step with:' + : 'This passage says the same thing as:', + ...duplicates.map( + ({passage, similarity}) => + `- ${passageLink(passage)} (${percent(similarity)} similar)` + ), + '', + onlyEdited ? EDITED_ADVICE : CONVENTION + ].join('\n') + }; + }); + return {event: 'COMMENT', body: '', comments}; +} + +const FORMATTERS = { + text: formatText, + json: pairs => + JSON.stringify( + pairs.map(({similarity, a, b}) => ({ + similarity, + a: describe(a), + b: describe(b) + })), + null, + '\t' + ) + '\n', + markdown: formatMarkdown +}; + +function main() { + const format = FORMATTERS[FORMAT]; + if (!format) { + throw new Error( + `Unknown --format "${FORMAT}"; use text, json, or markdown` + ); + } + + const passages = listDocsFiles().flatMap(file => + extractPassages( + file, + fs.readFileSync(path.join(ROOT_DIR, file), 'utf-8') + ) + ); + let pairs = findDuplicatePairs(passages); + if (BASELINE_FILE) { + pairs = markPreexisting(pairs, BASELINE_FILE); + } + // With a diff, a pre-existing pair the change edited is still reported, so + // the author hears about the other copy; without one, nothing says which + // pre-existing pairs matter, so they are all left out + pairs = DIFF + ? pairs.filter( + ({a, b}) => + addedLineIn(a) !== undefined || addedLineIn(b) !== undefined + ) + : pairs.filter(pair => !pair.preexisting); + if (REVIEW_FILE) { + fs.writeFileSync( + REVIEW_FILE, + JSON.stringify(reviewRequest(pairs), null, '\t') + '\n' + ); + } + + // Not process.exit(): that can truncate stdout when it is a pipe + process.stdout.write(format(pairs)); + process.exitCode = pairs.length === 0 ? 0 : 1; +} + +main(); diff --git a/docs/admin/how-to/enable-experimental-feature.mdx b/docs/admin/how-to/enable-experimental-feature.mdx index c8dfe8c0a..7129aefd9 100644 --- a/docs/admin/how-to/enable-experimental-feature.mdx +++ b/docs/admin/how-to/enable-experimental-feature.mdx @@ -26,6 +26,9 @@ This document will take you through how to add, enable, or disable an experiment 5. After changing the values in Administration > Global settings, the frontend will either restart automatically or you might be asked to restart the frontend for the changes to take effect. 6. For more information, see [Editing global settings for administrators](/admin/config/settings#editing-global-settings-for-site-admins) +After setting or changing certain values in **Site admin > Global settings** the frontend will restart automatically or +you might be asked to restart the frontend for the changes to take effect. + ## Further resources - [Sourcegraph - Configuration Settings](/admin/config/settings) diff --git a/docs/code-search/features.mdx b/docs/code-search/features.mdx index 0b39d7cb6..55a44a31f 100644 --- a/docs/code-search/features.mdx +++ b/docs/code-search/features.mdx @@ -57,7 +57,7 @@ Saved searches let you save and describe search queries so you can easily monito Search contexts help you search the code you care about on Sourcegraph. A search context represents a set of repositories at specific revisions on a Sourcegraph instance that will be targeted by search queries by default. -Every search on Sourcegraph uses a search context. Search contexts can be defined with the contexts selector shown in the search input, or entered directly in a search query. +Every search in Sourcegraph uses a search context. Search contexts can be defined with the contexts selector shown in the search input, or entered directly in a search query. See the [search contexts](/code-search/working/search-contexts) documentation to learn how to use and create search contexts.