diff --git a/app/components/crate-sidebar.css b/app/components/crate-sidebar.css index fed5290a268..57978a25b77 100644 --- a/app/components/crate-sidebar.css +++ b/app/components/crate-sidebar.css @@ -22,6 +22,7 @@ .msrv, .edition, .license, +.linecount, .bytes, .purl { display: flex; @@ -37,7 +38,8 @@ .date, .msrv, -.edition { +.edition, +.linecount { > span { cursor: help; } @@ -49,6 +51,7 @@ } } +.linecount, .bytes { font-variant-numeric: tabular-nums; } diff --git a/app/components/crate-sidebar.gjs b/app/components/crate-sidebar.gjs index 8ab8277a038..eb32fc3bae6 100644 --- a/app/components/crate-sidebar.gjs +++ b/app/components/crate-sidebar.gjs @@ -21,6 +21,7 @@ import Tooltip from 'crates-io/components/tooltip'; import dateFormat from 'crates-io/helpers/date-format'; import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; import dateFormatIso from 'crates-io/helpers/date-format-iso'; +import formatShortNum from 'crates-io/helpers/format-short-num'; import prettyBytes from 'crates-io/helpers/pretty-bytes'; import { simplifyUrl } from './crate-sidebar/link'; @@ -125,6 +126,20 @@ export default class CrateSidebar extends Component { {{/if}} + {{#if @version.linecounts.total_code_lines}} +
+ {{svgJar 'code'}} + + {{formatShortNum @version.linecounts.total_code_lines}} + SLoC + + Source Lines of Code
+ (excluding comments, integration tests and example code) +
+
+
+ {{/if}} + {{#if @version.crate_size}}
{{svgJar 'weight'}} diff --git a/app/helpers/format-short-num.js b/app/helpers/format-short-num.js new file mode 100644 index 00000000000..b74d3d85e3b --- /dev/null +++ b/app/helpers/format-short-num.js @@ -0,0 +1,41 @@ +import Helper from '@ember/component/helper'; +import { service } from '@ember/service'; + +/** + * This matches the implementation in https://github.com/rust-lang/crates_io_og_image/blob/v0.2.1/src/formatting.rs + * to ensure that we render roughly the same values in our user interface and the generated OpenGraph images. + */ +export default class FormatShortNumHelper extends Helper { + @service intl; + + compute([value]) { + const THRESHOLD = 1500; + const UNITS = ['', 'K', 'M']; + + let numValue = Number(value); + let unitIndex = 0; + + // Keep dividing by 1000 until value is below threshold or we've reached the last unit + while (numValue >= THRESHOLD && unitIndex < UNITS.length - 1) { + numValue /= 1000; + unitIndex += 1; + } + + let unit = UNITS[unitIndex]; + + // Special case for numbers without suffix - no decimal places + if (unitIndex === 0) { + return this.intl.formatNumber(value); + } + + // For K and M, format with appropriate decimal places + // Determine number of decimal places to keep number under 4 chars + let fractionDigits = numValue < 10 ? 1 : 0; + let number = this.intl.formatNumber(numValue, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + }); + + return number + unit; + } +} diff --git a/app/models/version.js b/app/models/version.js index 83c8cf0bf79..6d57a367f7e 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -25,6 +25,7 @@ export default class Version extends Model { @attr yanked; @attr license; @attr crate_size; + @attr linecounts; /** * The minimum supported Rust version of this crate version. diff --git a/app/services/intl.js b/app/services/intl.js index b9b80c7b3a9..94ed0db0704 100644 --- a/app/services/intl.js +++ b/app/services/intl.js @@ -4,7 +4,7 @@ export default class IntlService extends Service { // `undefined` means "use the default language of the browser" locale = undefined; - formatNumber(value) { - return Number(value).toLocaleString(this.locale); + formatNumber(value, options) { + return Number(value).toLocaleString(this.locale, options); } } diff --git a/e2e/acceptance/crate.spec.ts b/e2e/acceptance/crate.spec.ts index 3d18151c0a7..59daa2badf6 100644 --- a/e2e/acceptance/crate.spec.ts +++ b/e2e/acceptance/crate.spec.ts @@ -210,6 +210,18 @@ test.describe('Acceptance | crate page', { tag: '@acceptance' }, () => { await expect(page.locator('[data-test-license]')).toHaveText('MIT OR Apache-2.0'); }); + test('sidebar shows correct information', async ({ page, msw }) => { + let crate = msw.db.crate.create({ name: 'foo' }); + msw.db.version.create({ crate, num: '0.5.0' }); + msw.db.version.create({ crate, num: '1.0.0' }); + + await page.goto('/crates/foo'); + await expect(page.locator('[data-test-linecounts]')).toHaveText('1,119 SLoC'); + + await page.goto('/crates/foo/0.5.0'); + await expect(page.locator('[data-test-linecounts]')).toHaveText('520 SLoC'); + }); + test.skip('crates can be yanked by owner', async ({ page, msw }) => { loadFixtures(msw.db); diff --git a/packages/crates-io-msw/handlers/crates/downloads.test.js b/packages/crates-io-msw/handlers/crates/downloads.test.js index e588f787de7..2337366adc1 100644 --- a/packages/crates-io-msw/handlers/crates/downloads.test.js +++ b/packages/crates-io-msw/handlers/crates/downloads.test.js @@ -91,6 +91,22 @@ test('includes related versions', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', @@ -113,6 +129,27 @@ test('includes related versions', async function () { features: {}, id: 2, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.0.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.1/downloads', diff --git a/packages/crates-io-msw/handlers/crates/get.test.js b/packages/crates-io-msw/handlers/crates/get.test.js index 2dfa077b331..644a614e5b8 100644 --- a/packages/crates-io-msw/handlers/crates/get.test.js +++ b/packages/crates-io-msw/handlers/crates/get.test.js @@ -56,6 +56,22 @@ test('returns a crate object for known crates', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', @@ -121,6 +137,22 @@ test('works for non-canonical names', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo-bar/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/foo-bar/1.0.0-beta.1/downloads', @@ -159,6 +191,17 @@ test('includes related versions', async function () { downloads: 11_106, features: {}, license: 'MIT/Apache-2.0', + linecounts: { + languages: { + Python: { + code_lines: 421, + comment_lines: 64, + files: 8, + }, + }, + total_code_lines: 421, + total_comment_lines: 64, + }, links: { dependencies: '/api/v1/crates/rand/1.2.0/dependencies', version_downloads: '/api/v1/crates/rand/1.2.0/downloads', @@ -181,6 +224,27 @@ test('includes related versions', async function () { downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.1.0/dependencies', version_downloads: '/api/v1/crates/rand/1.1.0/downloads', @@ -203,6 +267,22 @@ test('includes related versions', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js index 88c6ec1642d..57a3eac95d1 100644 --- a/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js +++ b/packages/crates-io-msw/handlers/crates/reverse-dependencies.test.js @@ -76,6 +76,27 @@ test('returns a paginated list of crate versions depending to the specified crat downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/baz/1.0.1/dependencies', version_downloads: '/api/v1/crates/baz/1.0.1/downloads', @@ -98,6 +119,22 @@ test('returns a paginated list of crate versions depending to the specified crat downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/bar/1.0.0/dependencies', version_downloads: '/api/v1/crates/bar/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/versions/follow-updates.test.js b/packages/crates-io-msw/handlers/versions/follow-updates.test.js index 49d4ff3c9b6..614aa27adf4 100644 --- a/packages/crates-io-msw/handlers/versions/follow-updates.test.js +++ b/packages/crates-io-msw/handlers/versions/follow-updates.test.js @@ -33,6 +33,22 @@ test('returns latest versions of followed crates', async function () { downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.2.3/dependencies', version_downloads: '/api/v1/crates/foo/1.2.3/downloads', diff --git a/packages/crates-io-msw/handlers/versions/get.test.js b/packages/crates-io-msw/handlers/versions/get.test.js index 5a37d3eb422..9052552a345 100644 --- a/packages/crates-io-msw/handlers/versions/get.test.js +++ b/packages/crates-io-msw/handlers/versions/get.test.js @@ -34,6 +34,22 @@ test('returns a version object for known version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0-beta.1/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0-beta.1/downloads', diff --git a/packages/crates-io-msw/handlers/versions/list.test.js b/packages/crates-io-msw/handlers/versions/list.test.js index 01b733d6bf7..9787cc239f5 100644 --- a/packages/crates-io-msw/handlers/versions/list.test.js +++ b/packages/crates-io-msw/handlers/versions/list.test.js @@ -39,6 +39,17 @@ test('returns all versions belonging to the specified crate', async function () downloads: 11_106, features: {}, license: 'MIT/Apache-2.0', + linecounts: { + languages: { + Python: { + code_lines: 421, + comment_lines: 64, + files: 8, + }, + }, + total_code_lines: 421, + total_comment_lines: 64, + }, links: { dependencies: '/api/v1/crates/rand/1.2.0/dependencies', version_downloads: '/api/v1/crates/rand/1.2.0/downloads', @@ -61,6 +72,27 @@ test('returns all versions belonging to the specified crate', async function () downloads: 7404, features: {}, license: 'Apache-2.0', + linecounts: { + languages: { + CSS: { + code_lines: 503, + comment_lines: 42, + files: 2, + }, + Python: { + code_lines: 284, + comment_lines: 91, + files: 3, + }, + TypeScript: { + code_lines: 332, + comment_lines: 83, + files: 7, + }, + }, + total_code_lines: 1119, + total_comment_lines: 216, + }, links: { dependencies: '/api/v1/crates/rand/1.1.0/dependencies', version_downloads: '/api/v1/crates/rand/1.1.0/downloads', @@ -89,6 +121,22 @@ test('returns all versions belonging to the specified crate', async function () downloads: 3702, features: {}, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/rand/1.0.0/dependencies', version_downloads: '/api/v1/crates/rand/1.0.0/downloads', diff --git a/packages/crates-io-msw/handlers/versions/patch.test.js b/packages/crates-io-msw/handlers/versions/patch.test.js index 829c29c16e8..c2b5531fd6e 100644 --- a/packages/crates-io-msw/handlers/versions/patch.test.js +++ b/packages/crates-io-msw/handlers/versions/patch.test.js @@ -64,6 +64,22 @@ test('yanks the version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.0.0/dependencies', version_downloads: '/api/v1/crates/foo/1.0.0/downloads', @@ -95,6 +111,22 @@ test('yanks the version', async function () { features: {}, id: 1, license: 'MIT', + linecounts: { + languages: { + JavaScript: { + code_lines: 325, + comment_lines: 80, + files: 8, + }, + TypeScript: { + code_lines: 195, + comment_lines: 10, + files: 2, + }, + }, + total_code_lines: 520, + total_comment_lines: 90, + }, links: { dependencies: '/api/v1/crates/foo/1.0.0/dependencies', version_downloads: '/api/v1/crates/foo/1.0.0/downloads', diff --git a/packages/crates-io-msw/models/dependency.test.js b/packages/crates-io-msw/models/dependency.test.js index b9ad66d9b8c..ae9bc6adfcb 100644 --- a/packages/crates-io-msw/models/dependency.test.js +++ b/packages/crates-io-msw/models/dependency.test.js @@ -72,6 +72,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, diff --git a/packages/crates-io-msw/models/version-download.test.js b/packages/crates-io-msw/models/version-download.test.js index 0dd48be6bca..405b1878682 100644 --- a/packages/crates-io-msw/models/version-download.test.js +++ b/packages/crates-io-msw/models/version-download.test.js @@ -42,6 +42,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, diff --git a/packages/crates-io-msw/models/version.js b/packages/crates-io-msw/models/version.js index bf0b4ec22f9..54801e67698 100644 --- a/packages/crates-io-msw/models/version.js +++ b/packages/crates-io-msw/models/version.js @@ -4,6 +4,8 @@ import { applyDefault } from '../utils/defaults.js'; const LICENSES = ['MIT/Apache-2.0', 'MIT', 'Apache-2.0']; +const LANGUAGES = ['Rust', 'JavaScript', 'TypeScript', 'Python', 'CSS', 'HTML', 'Shell']; + export default { id: primaryKey(Number), @@ -19,6 +21,7 @@ export default { readme: nullable(String), rust_version: nullable(String), trustpub_data: nullable(Object), + linecounts: nullable(Object), crate: oneOf('crate'), publishedBy: nullable(oneOf('user')), @@ -36,9 +39,53 @@ export default { applyDefault(attrs, 'readme', () => null); applyDefault(attrs, 'rust_version', () => null); applyDefault(attrs, 'trustpub_data', () => null); + applyDefault(attrs, 'linecounts', () => generateLinecounts(attrs.id)); if (!attrs.crate) { throw new Error(`Missing \`crate\` relationship on \`version:${attrs.num}\``); } }, }; + +function generateLinecounts(id) { + // Some versions don't have linecount data (simulating older versions) + if (id % 4 === 0) { + return null; + } + + const languages = {}; + let totalCodeLines = 0; + let totalCommentLines = 0; + + // Generate 1-3 random languages per version + const numLanguages = (id % 3) + 1; + const selectedLanguages = []; + + for (let i = 0; i < numLanguages; i++) { + const langIndex = (id + i) % LANGUAGES.length; + selectedLanguages.push(LANGUAGES[langIndex]); + } + + for (const language of selectedLanguages) { + // Generate pseudo-random but deterministic line counts based on id and language + const seed = id + language.codePointAt(0); + const codeLines = ((seed * 137) % 500) + 50; // 50-550 lines + const commentLines = ((seed * 73) % 100) + 5; // 5-105 lines + const files = ((seed * 29) % 8) + 1; // 1-8 files + + languages[language] = { + code_lines: codeLines, + comment_lines: commentLines, + files: files, + }; + + totalCodeLines += codeLines; + totalCommentLines += commentLines; + } + + return { + languages, + total_code_lines: totalCodeLines, + total_comment_lines: totalCommentLines, + }; +} diff --git a/packages/crates-io-msw/models/version.test.js b/packages/crates-io-msw/models/version.test.js index 8d19e77f28d..9006f703c14 100644 --- a/packages/crates-io-msw/models/version.test.js +++ b/packages/crates-io-msw/models/version.test.js @@ -37,6 +37,22 @@ test('happy path', ({ expect }) => { "features": {}, "id": 1, "license": "MIT", + "linecounts": { + "languages": { + "JavaScript": { + "code_lines": 325, + "comment_lines": 80, + "files": 8, + }, + "TypeScript": { + "code_lines": 195, + "comment_lines": 10, + "files": 2, + }, + }, + "total_code_lines": 520, + "total_comment_lines": 90, + }, "num": "1.0.0", "publishedBy": null, "readme": null, diff --git a/public/assets/code.svg b/public/assets/code.svg new file mode 100644 index 00000000000..3dfae3ce114 --- /dev/null +++ b/public/assets/code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap index 115f1d84123..f24f7d0488d 100644 --- a/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap +++ b/src/snapshots/crates_io__openapi__tests__openapi_snapshot-2.snap @@ -1108,6 +1108,10 @@ expression: response.json() "null" ] }, + "linecounts": { + "description": "Line count statistics for this version.\n\nStatus: **Unstable**\n\nThis field may be `null` until the version has been analyzed, which\nhappens in an asynchronous background job.", + "type": "object" + }, "links": { "$ref": "#/components/schemas/VersionLinks", "description": "Links to other API endpoints related to this version." @@ -1190,7 +1194,8 @@ expression: response.json() "links", "crate_size", "audit_actions", - "checksum" + "checksum", + "linecounts" ], "type": "object" }, diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap index 3850557bc57..a96debf1df8 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__edition__edition_is_saved-4.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap index 8f18952f4ec..84e59b00384 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__links__crate_with_links_field-3.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": "git2", "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap index 5fe5f2e0f12..e2e20b682d4 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__boolean_readme-4.snap @@ -33,6 +33,11 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap index 606721884a8..a1efb78dcc5 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__manifest__lib_and_bin_crate-4.snap @@ -36,6 +36,17 @@ expression: response.json() "id": "[id]", "lib_links": null, "license": "MIT", + "linecounts": { + "languages": { + "Rust": { + "code_lines": 3, + "comment_lines": 0, + "files": 3 + } + }, + "total_code_lines": 3, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.0.0/authors", "dependencies": "/api/v1/crates/foo/1.0.0/dependencies", diff --git a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap index 1ad21be16e3..ea0ac3dfa2f 100644 --- a/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap +++ b/src/tests/krate/publish/snapshots/crates_io__tests__krate__publish__trustpub__full_flow-9.snap @@ -21,6 +21,11 @@ expression: response.json() "id": 2, "lib_links": null, "license": "MIT", + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + }, "links": { "authors": "/api/v1/crates/foo/1.1.0/authors", "dependencies": "/api/v1/crates/foo/1.1.0/dependencies", diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap index 9782ad19f8e..a4620b61f50 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-2.snap @@ -63,6 +63,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap index 3024140b782..9fa9757feab 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-3.snap @@ -74,6 +74,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap index 3024140b782..9fa9757feab 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-4.snap @@ -74,6 +74,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap index 8e3a9965e2a..4dcd1c8d08f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-5.snap @@ -85,6 +85,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap index 8e3a9965e2a..4dcd1c8d08f 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank-6.snap @@ -85,6 +85,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap index 9782ad19f8e..a4620b61f50 100644 --- a/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap +++ b/src/tests/krate/snapshots/crates_io__tests__krate__yanking__patch_version_yank_unyank.snap @@ -63,6 +63,11 @@ expression: json "homepage": null, "documentation": null, "repository": null, - "trustpub_data": null + "trustpub_data": null, + "linecounts": { + "languages": {}, + "total_code_lines": 0, + "total_comment_lines": 0 + } } } diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap index 6bc1c1257cb..174c12ac434 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__admin__index_include_yanked.snap @@ -1,7 +1,6 @@ --- source: src/tests/routes/crates/admin.rs expression: response.json() -snapshot_kind: text --- { "crates": [ diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap index c5d5669a769..85a9950f33f 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__include_default_version-2.snap @@ -55,6 +55,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_default_version/0.5.1/authors", "dependencies": "/api/v1/crates/foo_default_version/0.5.1/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap index 1db5c8d3bcc..0d8f97403d5 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show-2.snap @@ -68,6 +68,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.1/authors", "dependencies": "/api/v1/crates/foo_show/0.5.1/dependencies", @@ -107,6 +108,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.0/authors", "dependencies": "/api/v1/crates/foo_show/0.5.0/dependencies", @@ -146,6 +148,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/1.0.0/authors", "dependencies": "/api/v1/crates/foo_show/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap index b24044455e9..b18dbcdc4b9 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked-2.snap @@ -67,6 +67,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/0.5.0/authors", "dependencies": "/api/v1/crates/foo_show/0.5.0/dependencies", @@ -106,6 +107,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_show/1.0.0/authors", "dependencies": "/api/v1/crates/foo_show/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap index a28a59217da..0100fa322c8 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__prerelease_versions_not_included_in_reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c3/1.0.0/authors", "dependencies": "/api/v1/crates/c3/1.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap index 5a6e2f0bc53..cf5c91caf66 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/1.1.0/authors", "dependencies": "/api/v1/crates/c2/1.1.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap index 95e0eefb230..f189d3c5896 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_includes_published_by_user_when_present-2.snap @@ -51,6 +51,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c3/3.0.0/authors", "dependencies": "/api/v1/crates/c3/3.0.0/dependencies", @@ -90,6 +91,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap index 2f07702d00c..4c6614b2420 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_query_supports_u64_version_number_parts-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/1.0.18446744073709551615/authors", "dependencies": "/api/v1/crates/c2/1.0.18446744073709551615/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap index 0b9b5db1cf4..e0c4e01afe0 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__reverse_dependencies_when_old_version_doesnt_depend_but_new_does-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap index 0b9b5db1cf4..e0c4e01afe0 100644 --- a/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap +++ b/src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__reverse_dependencies__yanked_versions_not_included_in_reverse_dependencies-2.snap @@ -39,6 +39,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/c2/2.0.0/authors", "dependencies": "/api/v1/crates/c2/2.0.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap index 92ea2d62e20..521f406a88c 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__list__versions-2.snap @@ -26,6 +26,7 @@ expression: response.json() "id": 2, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/1.0.0/authors", "dependencies": "/api/v1/crates/foo_versions/1.0.0/dependencies", @@ -59,6 +60,7 @@ expression: response.json() "id": 1, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/0.5.1/authors", "dependencies": "/api/v1/crates/foo_versions/0.5.1/dependencies", @@ -98,6 +100,7 @@ expression: response.json() "id": 3, "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_versions/0.5.0/authors", "dependencies": "/api/v1/crates/foo_versions/0.5.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap index 3e9fd9dbe94..21df33f67dd 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap @@ -21,6 +21,7 @@ expression: json "id": "[id]", "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_vers_show_no_pb/1.0.0/authors", "dependencies": "/api/v1/crates/foo_vers_show_no_pb/1.0.0/dependencies", diff --git a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap index f174100a440..603951ed766 100644 --- a/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap +++ b/src/tests/routes/crates/versions/snapshots/crates_io__tests__routes__crates__versions__read__show_by_crate_name_and_version.snap @@ -21,6 +21,7 @@ expression: json "id": "[id]", "lib_links": null, "license": null, + "linecounts": null, "links": { "authors": "/api/v1/crates/foo_vers_show/2.0.0/authors", "dependencies": "/api/v1/crates/foo_vers_show/2.0.0/dependencies", diff --git a/src/views.rs b/src/views.rs index 481e3ec50f5..dc9095b289d 100644 --- a/src/views.rs +++ b/src/views.rs @@ -924,6 +924,15 @@ pub struct EncodableVersion { /// inside it. #[schema(value_type = Option)] pub trustpub_data: Option, + + /// Line count statistics for this version. + /// + /// Status: **Unstable** + /// + /// This field may be `null` until the version has been analyzed, which + /// happens in an asynchronous background job. + #[schema(value_type = Object)] + pub linecounts: Option, } impl EncodableVersion { @@ -955,6 +964,7 @@ impl EncodableVersion { documentation, repository, trustpub_data, + linecounts, .. } = version; @@ -990,6 +1000,7 @@ impl EncodableVersion { documentation, repository, trustpub_data, + linecounts, published_by: published_by.map(User::into), audit_actions: audit_actions .into_iter() @@ -1134,6 +1145,7 @@ mod tests { .and_utc(), }], trustpub_data: None, + linecounts: None, }; let json = serde_json::to_string(&ver).unwrap(); assert_some!(json.as_str().find(r#""updated_at":"2017-01-06T14:23:11Z""#)); diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index 5c5be2847f8..d2952da2678 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -306,4 +306,18 @@ module('Acceptance | crate page', function (hooks) { assert.strictEqual(currentURL(), '/crates/nanomsg'); assert.dom('[data-test-keyword]').exists(); }); + + test('sidebar shows correct information', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + let crate = this.db.crate.create({ name: 'foo' }); + this.db.version.create({ crate, num: '0.5.0' }); + this.db.version.create({ crate, num: '1.0.0' }); + + await visit('/crates/foo'); + assert.dom('[data-test-linecounts]').hasText('1,119 SLoC'); + + await visit('/crates/foo/0.5.0'); + assert.dom('[data-test-linecounts]').hasText('520 SLoC'); + }); }); diff --git a/tests/unit/helpers/format-short-num-test.gjs b/tests/unit/helpers/format-short-num-test.gjs new file mode 100644 index 00000000000..c28d9476998 --- /dev/null +++ b/tests/unit/helpers/format-short-num-test.gjs @@ -0,0 +1,48 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import formatShortNum from 'crates-io/helpers/format-short-num'; +import { setupRenderingTest } from 'crates-io/tests/helpers'; + +module('Unit | Helper | format-short-num', function (hooks) { + setupRenderingTest(hooks); + + async function check(assert, input, expected) { + await render(); + assert.dom().hasText(expected); + } + + test('formats numbers without suffix (below 1500)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 0, '0'); + await check(assert, 1, '1'); + await check(assert, 1000, '1,000'); + await check(assert, 1499, '1,499'); + }); + + test('formats numbers with K suffix (1500 to 1500000)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 1500, '1.5K'); + await check(assert, 2000, '2.0K'); + await check(assert, 5000, '5.0K'); + await check(assert, 10_000, '10K'); + await check(assert, 50_000, '50K'); + await check(assert, 100_000, '100K'); + await check(assert, 500_000, '500K'); + await check(assert, 999_999, '1,000K'); + }); + + test('formats numbers with M suffix (above 1500000)', async function (assert) { + this.owner.lookup('service:intl').locale = 'en'; + + await check(assert, 1_500_000, '1.5M'); + await check(assert, 2_000_000, '2.0M'); + await check(assert, 5_000_000, '5.0M'); + await check(assert, 10_000_000, '10M'); + await check(assert, 50_000_000, '50M'); + await check(assert, 100_000_000, '100M'); + await check(assert, 1_000_000_000, '1,000M'); + }); +});