diff --git a/package.json b/package.json index c9d4488..15700f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-git-diff", - "version": "0.0.16", + "version": "0.0.19", "description": "A parser for git diff", "main": "./build/cjs/index.js", "module": "./build/mjs/index.js", diff --git a/src/__fixtures__/31-no-prefix b/src/__fixtures__/31-no-prefix new file mode 100644 index 0000000..209812f --- /dev/null +++ b/src/__fixtures__/31-no-prefix @@ -0,0 +1,235 @@ +diff --git var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.test.ts var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.test.ts +new file mode 100644 +index 0000000..e69de29 +diff --git var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/playground/dn2ncwjsbmo/index.tsx var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.tsx +index 4d68325..fd576f7 100644 +--- var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/playground/dn2ncwjsbmo/index.tsx ++++ var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.tsx +@@ -1,190 +1,54 @@ +-import { createContext, useEffect, useState, use, useCallback } from 'react' ++import { Suspense, useSyncExternalStore } from 'react' + import * as ReactDOM from 'react-dom/client' +-import { +- type BlogPost, +- generateGradient, +- getMatchingPosts, +-} from '#shared/blog-posts' +-import { setGlobalSearchParams } from '#shared/utils' + +-type SearchParamsTuple = readonly [ +- URLSearchParams, +- typeof setGlobalSearchParams, +-] +-const SearchParamsContext = createContext<SearchParamsTuple>([ +- new URLSearchParams(window.location.search), +- setGlobalSearchParams, +-]) +- +-function SearchParamsProvider({ children }: { children: React.ReactNode }) { +- const [searchParams, setSearchParamsState] = useState( +- () => new URLSearchParams(window.location.search), +- ) ++export function makeMediaQueryStore(mediaQuery: string) { ++ function getSnapshot() { ++ return window.matchMedia(mediaQuery).matches ++ } + +- useEffect(() => { +- function updateSearchParams() { +- setSearchParamsState((prevParams) => { +- const newParams = new URLSearchParams(window.location.search) +- return prevParams.toString() === newParams.toString() +- ? prevParams +- : newParams +- }) ++ function subscribe(callback: () => void) { ++ const mediaQueryList = window.matchMedia(mediaQuery) ++ mediaQueryList.addEventListener('change', callback) ++ return () => { ++ mediaQueryList.removeEventListener('change', callback) + } +- window.addEventListener('popstate', updateSearchParams) +- return () => window.removeEventListener('popstate', updateSearchParams) +- }, []) +- +- const setSearchParams = useCallback( +- (...args: Parameters<typeof setGlobalSearchParams>) => { +- const searchParams = setGlobalSearchParams(...args) +- setSearchParamsState((prevParams) => { +- return prevParams.toString() === searchParams.toString() +- ? prevParams +- : searchParams +- }) +- return searchParams +- }, +- [], +- ) +- +- const searchParamsTuple = [searchParams, setSearchParams] as const +- +- return ( +- <SearchParamsContext value={searchParamsTuple}> +- {children} +- </SearchParamsContext> +- ) +-} +- +-function useSearchParams() { +- return use(SearchParamsContext) +-} +- +-const getQueryParam = (params: URLSearchParams) => params.get('query') ?? '' +- +-function App() { +- return ( +- <SearchParamsProvider> +- <div className="app"> +- <Form /> +- <MatchingPosts /> +- </div> +- </SearchParamsProvider> +- ) +-} +- +-function Form() { +- const [searchParams, setSearchParams] = useSearchParams() +- const query = getQueryParam(searchParams) +- +- const words = query.split(' ').map((w) => w.trim()) +- +- const dogChecked = words.includes('dog') +- const catChecked = words.includes('cat') +- const caterpillarChecked = words.includes('caterpillar') +- +- function handleCheck(tag: string, checked: boolean) { +- const newWords = checked ? [...words, tag] : words.filter((w) => w !== tag) +- setSearchParams( +- { query: newWords.filter(Boolean).join(' ').trim() }, +- { replace: true }, +- ) + } + +- return ( +- <form onSubmit={(e) => e.preventDefault()}> +- <div> +- <label htmlFor="searchInput">Search:</label> +- <input +- id="searchInput" +- name="query" +- type="search" +- value={query} +- onChange={(e) => +- setSearchParams({ query: e.currentTarget.value }, { replace: true }) +- } +- /> +- </div> +- <div> +- <label> +- <input +- type="checkbox" +- checked={dogChecked} +- onChange={(e) => handleCheck('dog', e.currentTarget.checked)} +- />{' '} +- 🐶 dog +- </label> +- <label> +- <input +- type="checkbox" +- checked={catChecked} +- onChange={(e) => handleCheck('cat', e.currentTarget.checked)} +- />{' '} +- 🐱 cat +- </label> +- <label> +- <input +- type="checkbox" +- checked={caterpillarChecked} +- onChange={(e) => +- handleCheck('caterpillar', e.currentTarget.checked) +- } +- />{' '} +- 🐛 caterpillar +- </label> +- </div> +- </form> +- ) ++ return function useMediaQuery() { ++ return useSyncExternalStore(subscribe, getSnapshot) ++ } + } + +-function MatchingPosts() { +- const [searchParams] = useSearchParams() +- const query = getQueryParam(searchParams) +- const matchingPosts = getMatchingPosts(query) ++const useNarrowMediaQuery = makeMediaQueryStore('(max-width: 600px)') + +- return ( +- <ul className="post-list"> +- {matchingPosts.map((post) => ( +- <Card key={post.id} post={post} /> +- ))} +- </ul> +- ) ++function NarrowScreenNotifier() { ++ const isNarrow = useNarrowMediaQuery() ++ return isNarrow ? 'You are on a narrow screen' : 'You are on a wide screen' + } + +-function Card({ post }: { post: BlogPost }) { +- const [isFavorited, setIsFavorited] = useState(false) ++function App() { + return ( +- <li> +- {isFavorited ? ( +- <button +- aria-label="Remove favorite" +- onClick={() => setIsFavorited(false)} +- > +- ❤️ +- </button> +- ) : ( +- <button aria-label="Add favorite" onClick={() => setIsFavorited(true)}> +- 🤍 +- </button> +- )} +- <div +- className="post-image" +- style={{ background: generateGradient(post.id) }} +- /> +- <a +- href={post.id} +- onClick={(event) => { +- event.preventDefault() +- alert(`Great! Let's go to ${post.id}!`) +- }} +- > +- <h2>{post.title}</h2> +- <p>{post.description}</p> +- </a> +- </li> ++ <div> ++ <div>This is your narrow screen state:</div> ++ <Suspense fallback=""> ++ <NarrowScreenNotifier /> ++ </Suspense> ++ </div> + ) + } + + const rootEl = document.createElement('div') + document.body.append(rootEl) +-ReactDOM.createRoot(rootEl).render(<App />) ++// 🦉 here's how we pretend we're server-rendering ++rootEl.innerHTML = (await import('react-dom/server')).renderToString(<App />) ++ ++// 🦉 here's how we simulate a delay in hydrating with client-side js ++await new Promise((resolve) => setTimeout(resolve, 1000)) ++ ++ReactDOM.hydrateRoot(rootEl, <App />, { ++ onRecoverableError(error) { ++ if (String(error).includes('Missing getServerSnapshot')) return ++ ++ console.error(error) ++ }, ++}) \ No newline at end of file diff --git a/src/__fixtures__/added-empty-file b/src/__fixtures__/added-empty-file new file mode 100644 index 0000000..b8e4b8f --- /dev/null +++ b/src/__fixtures__/added-empty-file @@ -0,0 +1,3 @@ +diff --git a/src/empty b/src/empty +new file mode 100644 +index 0000000..e69de29 \ No newline at end of file diff --git a/src/__fixtures__/consecutive-empty-files b/src/__fixtures__/consecutive-empty-files new file mode 100644 index 0000000..98a3f8e --- /dev/null +++ b/src/__fixtures__/consecutive-empty-files @@ -0,0 +1,14 @@ +diff --git a/content b/content +new file mode 100644 +index 0000000..6b584e8 +--- /dev/null ++++ b/content +@@ -0,0 +1 @@ ++content +\ No newline at end of file +diff --git a/empty b/empty +new file mode 100644 +index 0000000..e69de29 +diff --git a/empty2 b/empty2 +new file mode 100644 +index 0000000..e69de29 \ No newline at end of file diff --git a/src/__fixtures__/deleted-empty-file b/src/__fixtures__/deleted-empty-file new file mode 100644 index 0000000..94c6d26 --- /dev/null +++ b/src/__fixtures__/deleted-empty-file @@ -0,0 +1,3 @@ +diff --git a/src/empty b/src/empty +deleted file mode 100644 +index e69de29..0000000 \ No newline at end of file diff --git a/src/__fixtures__/renamed-empty b/src/__fixtures__/renamed-empty new file mode 100644 index 0000000..8fee8b8 --- /dev/null +++ b/src/__fixtures__/renamed-empty @@ -0,0 +1,4 @@ +diff --git a/src/empty b/src/renamed-empty +similarity index 100% +rename from src/empty +rename to src/renamed-empty \ No newline at end of file diff --git a/src/__fixtures__/unified b/src/__fixtures__/unified new file mode 100644 index 0000000..9c7e37f --- /dev/null +++ b/src/__fixtures__/unified @@ -0,0 +1,6 @@ +diff --git a/test.txt b/test.txt +index 27b30e2..62944e1 100644 +--- a/test.txt ++++ b/test.txt +@@ -2,0 +3 @@ bbb ++ddd \ No newline at end of file diff --git a/src/__tests__/31-no-prefix.test.ts b/src/__tests__/31-no-prefix.test.ts new file mode 100644 index 0000000..f620690 --- /dev/null +++ b/src/__tests__/31-no-prefix.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('issue 31-no-prefix', () => { + const fixture = getFixture('31-no-prefix'); + + it('parse `31-no-prefix`', () => { + expect(parseGitDiff(fixture, { noPrefix: true })).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/31.test.ts b/src/__tests__/31.test.ts index b073b88..4c77c1b 100644 --- a/src/__tests__/31.test.ts +++ b/src/__tests__/31.test.ts @@ -1,7 +1,7 @@ import { getFixture } from './test-utils'; import parseGitDiff from '../parse-git-diff'; -describe.only('issue 31', () => { +describe('issue 31', () => { const fixture = getFixture('31'); it('parse `31`', () => { diff --git a/src/__tests__/__snapshots__/31-no-prefix.test.ts.snap b/src/__tests__/__snapshots__/31-no-prefix.test.ts.snap new file mode 100644 index 0000000..f6f0ad0 --- /dev/null +++ b/src/__tests__/__snapshots__/31-no-prefix.test.ts.snap @@ -0,0 +1,1186 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`issue 31-no-prefix parse \`31-no-prefix\` 1`] = ` +{ + "files": [ + { + "chunks": [], + "path": "var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.test.ts", + "type": "AddedFile", + }, + { + "chunks": [ + { + "changes": [ + { + "content": "import { createContext, useEffect, useState, use, useCallback } from 'react'", + "lineBefore": 1, + "type": "DeletedLine", + }, + { + "content": "import { Suspense, useSyncExternalStore } from 'react'", + "lineAfter": 1, + "type": "AddedLine", + }, + { + "content": "import * as ReactDOM from 'react-dom/client'", + "lineAfter": 2, + "lineBefore": 2, + "type": "UnchangedLine", + }, + { + "content": "import {", + "lineBefore": 3, + "type": "DeletedLine", + }, + { + "content": " type BlogPost,", + "lineBefore": 4, + "type": "DeletedLine", + }, + { + "content": " generateGradient,", + "lineBefore": 5, + "type": "DeletedLine", + }, + { + "content": " getMatchingPosts,", + "lineBefore": 6, + "type": "DeletedLine", + }, + { + "content": "} from '#shared/blog-posts'", + "lineBefore": 7, + "type": "DeletedLine", + }, + { + "content": "import { setGlobalSearchParams } from '#shared/utils'", + "lineBefore": 8, + "type": "DeletedLine", + }, + { + "content": "", + "lineAfter": 3, + "lineBefore": 9, + "type": "UnchangedLine", + }, + { + "content": "type SearchParamsTuple = readonly [", + "lineBefore": 10, + "type": "DeletedLine", + }, + { + "content": " URLSearchParams,", + "lineBefore": 11, + "type": "DeletedLine", + }, + { + "content": " typeof setGlobalSearchParams,", + "lineBefore": 12, + "type": "DeletedLine", + }, + { + "content": "]", + "lineBefore": 13, + "type": "DeletedLine", + }, + { + "content": "const SearchParamsContext = createContext<SearchParamsTuple>([", + "lineBefore": 14, + "type": "DeletedLine", + }, + { + "content": " new URLSearchParams(window.location.search),", + "lineBefore": 15, + "type": "DeletedLine", + }, + { + "content": " setGlobalSearchParams,", + "lineBefore": 16, + "type": "DeletedLine", + }, + { + "content": "])", + "lineBefore": 17, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 18, + "type": "DeletedLine", + }, + { + "content": "function SearchParamsProvider({ children }: { children: React.ReactNode }) {", + "lineBefore": 19, + "type": "DeletedLine", + }, + { + "content": " const [searchParams, setSearchParamsState] = useState(", + "lineBefore": 20, + "type": "DeletedLine", + }, + { + "content": " () => new URLSearchParams(window.location.search),", + "lineBefore": 21, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 22, + "type": "DeletedLine", + }, + { + "content": "export function makeMediaQueryStore(mediaQuery: string) {", + "lineAfter": 4, + "type": "AddedLine", + }, + { + "content": " function getSnapshot() {", + "lineAfter": 5, + "type": "AddedLine", + }, + { + "content": " return window.matchMedia(mediaQuery).matches", + "lineAfter": 6, + "type": "AddedLine", + }, + { + "content": " }", + "lineAfter": 7, + "type": "AddedLine", + }, + { + "content": "", + "lineAfter": 8, + "lineBefore": 23, + "type": "UnchangedLine", + }, + { + "content": " useEffect(() => {", + "lineBefore": 24, + "type": "DeletedLine", + }, + { + "content": " function updateSearchParams() {", + "lineBefore": 25, + "type": "DeletedLine", + }, + { + "content": " setSearchParamsState((prevParams) => {", + "lineBefore": 26, + "type": "DeletedLine", + }, + { + "content": " const newParams = new URLSearchParams(window.location.search)", + "lineBefore": 27, + "type": "DeletedLine", + }, + { + "content": " return prevParams.toString() === newParams.toString()", + "lineBefore": 28, + "type": "DeletedLine", + }, + { + "content": " ? prevParams", + "lineBefore": 29, + "type": "DeletedLine", + }, + { + "content": " : newParams", + "lineBefore": 30, + "type": "DeletedLine", + }, + { + "content": " })", + "lineBefore": 31, + "type": "DeletedLine", + }, + { + "content": " function subscribe(callback: () => void) {", + "lineAfter": 9, + "type": "AddedLine", + }, + { + "content": " const mediaQueryList = window.matchMedia(mediaQuery)", + "lineAfter": 10, + "type": "AddedLine", + }, + { + "content": " mediaQueryList.addEventListener('change', callback)", + "lineAfter": 11, + "type": "AddedLine", + }, + { + "content": " return () => {", + "lineAfter": 12, + "type": "AddedLine", + }, + { + "content": " mediaQueryList.removeEventListener('change', callback)", + "lineAfter": 13, + "type": "AddedLine", + }, + { + "content": " }", + "lineAfter": 14, + "lineBefore": 32, + "type": "UnchangedLine", + }, + { + "content": " window.addEventListener('popstate', updateSearchParams)", + "lineBefore": 33, + "type": "DeletedLine", + }, + { + "content": " return () => window.removeEventListener('popstate', updateSearchParams)", + "lineBefore": 34, + "type": "DeletedLine", + }, + { + "content": " }, [])", + "lineBefore": 35, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 36, + "type": "DeletedLine", + }, + { + "content": " const setSearchParams = useCallback(", + "lineBefore": 37, + "type": "DeletedLine", + }, + { + "content": " (...args: Parameters<typeof setGlobalSearchParams>) => {", + "lineBefore": 38, + "type": "DeletedLine", + }, + { + "content": " const searchParams = setGlobalSearchParams(...args)", + "lineBefore": 39, + "type": "DeletedLine", + }, + { + "content": " setSearchParamsState((prevParams) => {", + "lineBefore": 40, + "type": "DeletedLine", + }, + { + "content": " return prevParams.toString() === searchParams.toString()", + "lineBefore": 41, + "type": "DeletedLine", + }, + { + "content": " ? prevParams", + "lineBefore": 42, + "type": "DeletedLine", + }, + { + "content": " : searchParams", + "lineBefore": 43, + "type": "DeletedLine", + }, + { + "content": " })", + "lineBefore": 44, + "type": "DeletedLine", + }, + { + "content": " return searchParams", + "lineBefore": 45, + "type": "DeletedLine", + }, + { + "content": " },", + "lineBefore": 46, + "type": "DeletedLine", + }, + { + "content": " [],", + "lineBefore": 47, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 48, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 49, + "type": "DeletedLine", + }, + { + "content": " const searchParamsTuple = [searchParams, setSearchParams] as const", + "lineBefore": 50, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 51, + "type": "DeletedLine", + }, + { + "content": " return (", + "lineBefore": 52, + "type": "DeletedLine", + }, + { + "content": " <SearchParamsContext value={searchParamsTuple}>", + "lineBefore": 53, + "type": "DeletedLine", + }, + { + "content": " {children}", + "lineBefore": 54, + "type": "DeletedLine", + }, + { + "content": " </SearchParamsContext>", + "lineBefore": 55, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 56, + "type": "DeletedLine", + }, + { + "content": "}", + "lineBefore": 57, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 58, + "type": "DeletedLine", + }, + { + "content": "function useSearchParams() {", + "lineBefore": 59, + "type": "DeletedLine", + }, + { + "content": " return use(SearchParamsContext)", + "lineBefore": 60, + "type": "DeletedLine", + }, + { + "content": "}", + "lineBefore": 61, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 62, + "type": "DeletedLine", + }, + { + "content": "const getQueryParam = (params: URLSearchParams) => params.get('query') ?? ''", + "lineBefore": 63, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 64, + "type": "DeletedLine", + }, + { + "content": "function App() {", + "lineBefore": 65, + "type": "DeletedLine", + }, + { + "content": " return (", + "lineBefore": 66, + "type": "DeletedLine", + }, + { + "content": " <SearchParamsProvider>", + "lineBefore": 67, + "type": "DeletedLine", + }, + { + "content": " <div className="app">", + "lineBefore": 68, + "type": "DeletedLine", + }, + { + "content": " <Form />", + "lineBefore": 69, + "type": "DeletedLine", + }, + { + "content": " <MatchingPosts />", + "lineBefore": 70, + "type": "DeletedLine", + }, + { + "content": " </div>", + "lineBefore": 71, + "type": "DeletedLine", + }, + { + "content": " </SearchParamsProvider>", + "lineBefore": 72, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 73, + "type": "DeletedLine", + }, + { + "content": "}", + "lineBefore": 74, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 75, + "type": "DeletedLine", + }, + { + "content": "function Form() {", + "lineBefore": 76, + "type": "DeletedLine", + }, + { + "content": " const [searchParams, setSearchParams] = useSearchParams()", + "lineBefore": 77, + "type": "DeletedLine", + }, + { + "content": " const query = getQueryParam(searchParams)", + "lineBefore": 78, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 79, + "type": "DeletedLine", + }, + { + "content": " const words = query.split(' ').map((w) => w.trim())", + "lineBefore": 80, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 81, + "type": "DeletedLine", + }, + { + "content": " const dogChecked = words.includes('dog')", + "lineBefore": 82, + "type": "DeletedLine", + }, + { + "content": " const catChecked = words.includes('cat')", + "lineBefore": 83, + "type": "DeletedLine", + }, + { + "content": " const caterpillarChecked = words.includes('caterpillar')", + "lineBefore": 84, + "type": "DeletedLine", + }, + { + "content": "", + "lineBefore": 85, + "type": "DeletedLine", + }, + { + "content": " function handleCheck(tag: string, checked: boolean) {", + "lineBefore": 86, + "type": "DeletedLine", + }, + { + "content": " const newWords = checked ? [...words, tag] : words.filter((w) => w !== tag)", + "lineBefore": 87, + "type": "DeletedLine", + }, + { + "content": " setSearchParams(", + "lineBefore": 88, + "type": "DeletedLine", + }, + { + "content": " { query: newWords.filter(Boolean).join(' ').trim() },", + "lineBefore": 89, + "type": "DeletedLine", + }, + { + "content": " { replace: true },", + "lineBefore": 90, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 91, + "type": "DeletedLine", + }, + { + "content": " }", + "lineAfter": 15, + "lineBefore": 92, + "type": "UnchangedLine", + }, + { + "content": "", + "lineAfter": 16, + "lineBefore": 93, + "type": "UnchangedLine", + }, + { + "content": " return (", + "lineBefore": 94, + "type": "DeletedLine", + }, + { + "content": " <form onSubmit={(e) => e.preventDefault()}>", + "lineBefore": 95, + "type": "DeletedLine", + }, + { + "content": " <div>", + "lineBefore": 96, + "type": "DeletedLine", + }, + { + "content": " <label htmlFor="searchInput">Search:</label>", + "lineBefore": 97, + "type": "DeletedLine", + }, + { + "content": " <input", + "lineBefore": 98, + "type": "DeletedLine", + }, + { + "content": " id="searchInput"", + "lineBefore": 99, + "type": "DeletedLine", + }, + { + "content": " name="query"", + "lineBefore": 100, + "type": "DeletedLine", + }, + { + "content": " type="search"", + "lineBefore": 101, + "type": "DeletedLine", + }, + { + "content": " value={query}", + "lineBefore": 102, + "type": "DeletedLine", + }, + { + "content": " onChange={(e) =>", + "lineBefore": 103, + "type": "DeletedLine", + }, + { + "content": " setSearchParams({ query: e.currentTarget.value }, { replace: true })", + "lineBefore": 104, + "type": "DeletedLine", + }, + { + "content": " }", + "lineBefore": 105, + "type": "DeletedLine", + }, + { + "content": " />", + "lineBefore": 106, + "type": "DeletedLine", + }, + { + "content": " </div>", + "lineBefore": 107, + "type": "DeletedLine", + }, + { + "content": " <div>", + "lineBefore": 108, + "type": "DeletedLine", + }, + { + "content": " <label>", + "lineBefore": 109, + "type": "DeletedLine", + }, + { + "content": " <input", + "lineBefore": 110, + "type": "DeletedLine", + }, + { + "content": " type="checkbox"", + "lineBefore": 111, + "type": "DeletedLine", + }, + { + "content": " checked={dogChecked}", + "lineBefore": 112, + "type": "DeletedLine", + }, + { + "content": " onChange={(e) => handleCheck('dog', e.currentTarget.checked)}", + "lineBefore": 113, + "type": "DeletedLine", + }, + { + "content": " />{' '}", + "lineBefore": 114, + "type": "DeletedLine", + }, + { + "content": " 🐶 dog", + "lineBefore": 115, + "type": "DeletedLine", + }, + { + "content": " </label>", + "lineBefore": 116, + "type": "DeletedLine", + }, + { + "content": " <label>", + "lineBefore": 117, + "type": "DeletedLine", + }, + { + "content": " <input", + "lineBefore": 118, + "type": "DeletedLine", + }, + { + "content": " type="checkbox"", + "lineBefore": 119, + "type": "DeletedLine", + }, + { + "content": " checked={catChecked}", + "lineBefore": 120, + "type": "DeletedLine", + }, + { + "content": " onChange={(e) => handleCheck('cat', e.currentTarget.checked)}", + "lineBefore": 121, + "type": "DeletedLine", + }, + { + "content": " />{' '}", + "lineBefore": 122, + "type": "DeletedLine", + }, + { + "content": " 🐱 cat", + "lineBefore": 123, + "type": "DeletedLine", + }, + { + "content": " </label>", + "lineBefore": 124, + "type": "DeletedLine", + }, + { + "content": " <label>", + "lineBefore": 125, + "type": "DeletedLine", + }, + { + "content": " <input", + "lineBefore": 126, + "type": "DeletedLine", + }, + { + "content": " type="checkbox"", + "lineBefore": 127, + "type": "DeletedLine", + }, + { + "content": " checked={caterpillarChecked}", + "lineBefore": 128, + "type": "DeletedLine", + }, + { + "content": " onChange={(e) =>", + "lineBefore": 129, + "type": "DeletedLine", + }, + { + "content": " handleCheck('caterpillar', e.currentTarget.checked)", + "lineBefore": 130, + "type": "DeletedLine", + }, + { + "content": " }", + "lineBefore": 131, + "type": "DeletedLine", + }, + { + "content": " />{' '}", + "lineBefore": 132, + "type": "DeletedLine", + }, + { + "content": " 🐛 caterpillar", + "lineBefore": 133, + "type": "DeletedLine", + }, + { + "content": " </label>", + "lineBefore": 134, + "type": "DeletedLine", + }, + { + "content": " </div>", + "lineBefore": 135, + "type": "DeletedLine", + }, + { + "content": " </form>", + "lineBefore": 136, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 137, + "type": "DeletedLine", + }, + { + "content": " return function useMediaQuery() {", + "lineAfter": 17, + "type": "AddedLine", + }, + { + "content": " return useSyncExternalStore(subscribe, getSnapshot)", + "lineAfter": 18, + "type": "AddedLine", + }, + { + "content": " }", + "lineAfter": 19, + "type": "AddedLine", + }, + { + "content": "}", + "lineAfter": 20, + "lineBefore": 138, + "type": "UnchangedLine", + }, + { + "content": "", + "lineAfter": 21, + "lineBefore": 139, + "type": "UnchangedLine", + }, + { + "content": "function MatchingPosts() {", + "lineBefore": 140, + "type": "DeletedLine", + }, + { + "content": " const [searchParams] = useSearchParams()", + "lineBefore": 141, + "type": "DeletedLine", + }, + { + "content": " const query = getQueryParam(searchParams)", + "lineBefore": 142, + "type": "DeletedLine", + }, + { + "content": " const matchingPosts = getMatchingPosts(query)", + "lineBefore": 143, + "type": "DeletedLine", + }, + { + "content": "const useNarrowMediaQuery = makeMediaQueryStore('(max-width: 600px)')", + "lineAfter": 22, + "type": "AddedLine", + }, + { + "content": "", + "lineAfter": 23, + "lineBefore": 144, + "type": "UnchangedLine", + }, + { + "content": " return (", + "lineBefore": 145, + "type": "DeletedLine", + }, + { + "content": " <ul className="post-list">", + "lineBefore": 146, + "type": "DeletedLine", + }, + { + "content": " {matchingPosts.map((post) => (", + "lineBefore": 147, + "type": "DeletedLine", + }, + { + "content": " <Card key={post.id} post={post} />", + "lineBefore": 148, + "type": "DeletedLine", + }, + { + "content": " ))}", + "lineBefore": 149, + "type": "DeletedLine", + }, + { + "content": " </ul>", + "lineBefore": 150, + "type": "DeletedLine", + }, + { + "content": " )", + "lineBefore": 151, + "type": "DeletedLine", + }, + { + "content": "function NarrowScreenNotifier() {", + "lineAfter": 24, + "type": "AddedLine", + }, + { + "content": " const isNarrow = useNarrowMediaQuery()", + "lineAfter": 25, + "type": "AddedLine", + }, + { + "content": " return isNarrow ? 'You are on a narrow screen' : 'You are on a wide screen'", + "lineAfter": 26, + "type": "AddedLine", + }, + { + "content": "}", + "lineAfter": 27, + "lineBefore": 152, + "type": "UnchangedLine", + }, + { + "content": "", + "lineAfter": 28, + "lineBefore": 153, + "type": "UnchangedLine", + }, + { + "content": "function Card({ post }: { post: BlogPost }) {", + "lineBefore": 154, + "type": "DeletedLine", + }, + { + "content": " const [isFavorited, setIsFavorited] = useState(false)", + "lineBefore": 155, + "type": "DeletedLine", + }, + { + "content": "function App() {", + "lineAfter": 29, + "type": "AddedLine", + }, + { + "content": " return (", + "lineAfter": 30, + "lineBefore": 156, + "type": "UnchangedLine", + }, + { + "content": " <li>", + "lineBefore": 157, + "type": "DeletedLine", + }, + { + "content": " {isFavorited ? (", + "lineBefore": 158, + "type": "DeletedLine", + }, + { + "content": " <button", + "lineBefore": 159, + "type": "DeletedLine", + }, + { + "content": " aria-label="Remove favorite"", + "lineBefore": 160, + "type": "DeletedLine", + }, + { + "content": " onClick={() => setIsFavorited(false)}", + "lineBefore": 161, + "type": "DeletedLine", + }, + { + "content": " >", + "lineBefore": 162, + "type": "DeletedLine", + }, + { + "content": " ❤️", + "lineBefore": 163, + "type": "DeletedLine", + }, + { + "content": " </button>", + "lineBefore": 164, + "type": "DeletedLine", + }, + { + "content": " ) : (", + "lineBefore": 165, + "type": "DeletedLine", + }, + { + "content": " <button aria-label="Add favorite" onClick={() => setIsFavorited(true)}>", + "lineBefore": 166, + "type": "DeletedLine", + }, + { + "content": " 🤍", + "lineBefore": 167, + "type": "DeletedLine", + }, + { + "content": " </button>", + "lineBefore": 168, + "type": "DeletedLine", + }, + { + "content": " )}", + "lineBefore": 169, + "type": "DeletedLine", + }, + { + "content": " <div", + "lineBefore": 170, + "type": "DeletedLine", + }, + { + "content": " className="post-image"", + "lineBefore": 171, + "type": "DeletedLine", + }, + { + "content": " style={{ background: generateGradient(post.id) }}", + "lineBefore": 172, + "type": "DeletedLine", + }, + { + "content": " />", + "lineBefore": 173, + "type": "DeletedLine", + }, + { + "content": " <a", + "lineBefore": 174, + "type": "DeletedLine", + }, + { + "content": " href={post.id}", + "lineBefore": 175, + "type": "DeletedLine", + }, + { + "content": " onClick={(event) => {", + "lineBefore": 176, + "type": "DeletedLine", + }, + { + "content": " event.preventDefault()", + "lineBefore": 177, + "type": "DeletedLine", + }, + { + "content": " alert(\`Great! Let's go to \${post.id}!\`)", + "lineBefore": 178, + "type": "DeletedLine", + }, + { + "content": " }}", + "lineBefore": 179, + "type": "DeletedLine", + }, + { + "content": " >", + "lineBefore": 180, + "type": "DeletedLine", + }, + { + "content": " <h2>{post.title}</h2>", + "lineBefore": 181, + "type": "DeletedLine", + }, + { + "content": " <p>{post.description}</p>", + "lineBefore": 182, + "type": "DeletedLine", + }, + { + "content": " </a>", + "lineBefore": 183, + "type": "DeletedLine", + }, + { + "content": " </li>", + "lineBefore": 184, + "type": "DeletedLine", + }, + { + "content": " <div>", + "lineAfter": 31, + "type": "AddedLine", + }, + { + "content": " <div>This is your narrow screen state:</div>", + "lineAfter": 32, + "type": "AddedLine", + }, + { + "content": " <Suspense fallback="">", + "lineAfter": 33, + "type": "AddedLine", + }, + { + "content": " <NarrowScreenNotifier />", + "lineAfter": 34, + "type": "AddedLine", + }, + { + "content": " </Suspense>", + "lineAfter": 35, + "type": "AddedLine", + }, + { + "content": " </div>", + "lineAfter": 36, + "type": "AddedLine", + }, + { + "content": " )", + "lineAfter": 37, + "lineBefore": 185, + "type": "UnchangedLine", + }, + { + "content": "}", + "lineAfter": 38, + "lineBefore": 186, + "type": "UnchangedLine", + }, + { + "content": "", + "lineAfter": 39, + "lineBefore": 187, + "type": "UnchangedLine", + }, + { + "content": "const rootEl = document.createElement('div')", + "lineAfter": 40, + "lineBefore": 188, + "type": "UnchangedLine", + }, + { + "content": "document.body.append(rootEl)", + "lineAfter": 41, + "lineBefore": 189, + "type": "UnchangedLine", + }, + { + "content": "ReactDOM.createRoot(rootEl).render(<App />)", + "lineBefore": 190, + "type": "DeletedLine", + }, + { + "content": "// 🦉 here's how we pretend we're server-rendering", + "lineAfter": 42, + "type": "AddedLine", + }, + { + "content": "rootEl.innerHTML = (await import('react-dom/server')).renderToString(<App />)", + "lineAfter": 43, + "type": "AddedLine", + }, + { + "content": "", + "lineAfter": 44, + "type": "AddedLine", + }, + { + "content": "// 🦉 here's how we simulate a delay in hydrating with client-side js", + "lineAfter": 45, + "type": "AddedLine", + }, + { + "content": "await new Promise((resolve) => setTimeout(resolve, 1000))", + "lineAfter": 46, + "type": "AddedLine", + }, + { + "content": "", + "lineAfter": 47, + "type": "AddedLine", + }, + { + "content": "ReactDOM.hydrateRoot(rootEl, <App />, {", + "lineAfter": 48, + "type": "AddedLine", + }, + { + "content": " onRecoverableError(error) {", + "lineAfter": 49, + "type": "AddedLine", + }, + { + "content": " if (String(error).includes('Missing getServerSnapshot')) return", + "lineAfter": 50, + "type": "AddedLine", + }, + { + "content": "", + "lineAfter": 51, + "type": "AddedLine", + }, + { + "content": " console.error(error)", + "lineAfter": 52, + "type": "AddedLine", + }, + { + "content": " },", + "lineAfter": 53, + "type": "AddedLine", + }, + { + "content": "})", + "lineAfter": 54, + "type": "AddedLine", + }, + ], + "context": undefined, + "fromFileRange": { + "lines": 190, + "start": 1, + }, + "toFileRange": { + "lines": 54, + "start": 1, + }, + "type": "Chunk", + }, + ], + "path": "var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/09.03.solution/dn2ncwjsbmo/index.tsx", + "type": "ChangedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/__snapshots__/31.test.ts.snap b/src/__tests__/__snapshots__/31.test.ts.snap index 0df8961..074b94b 100644 --- a/src/__tests__/__snapshots__/31.test.ts.snap +++ b/src/__tests__/__snapshots__/31.test.ts.snap @@ -3,6 +3,11 @@ exports[`issue 31 parse \`31\` 1`] = ` { "files": [ + { + "chunks": [], + "path": "var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/04.01.solution/7h2jowvfi2q/index.test.tsx", + "type": "AddedFile", + }, { "chunks": [ { @@ -406,7 +411,7 @@ exports[`issue 31 parse \`31\` 1`] = ` }, ], "path": "var/folders/kt/zd3bfncd0c3gjx25hbcq483c0000gn/T/epicshop/diff/advanced-react-apis/04.01.solution/7h2jowvfi2q/index.tsx", - "type": "AddedFile", + "type": "ChangedFile", }, ], "type": "GitDiff", diff --git a/src/__tests__/__snapshots__/added-empty-file.test.ts.snap b/src/__tests__/__snapshots__/added-empty-file.test.ts.snap new file mode 100644 index 0000000..44f1615 --- /dev/null +++ b/src/__tests__/__snapshots__/added-empty-file.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`added-empty-file parse \`added-empty-file\` 1`] = ` +{ + "files": [ + { + "chunks": [], + "path": "src/empty", + "type": "AddedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/__snapshots__/chunk-context.test.ts.snap b/src/__tests__/__snapshots__/chunk-context.test.ts.snap index f2fa843..85ef29e 100644 --- a/src/__tests__/__snapshots__/chunk-context.test.ts.snap +++ b/src/__tests__/__snapshots__/chunk-context.test.ts.snap @@ -14,14 +14,14 @@ exports[`chunk-context parse \`chunk-context\` 1`] = ` "type": "UnchangedLine", }, { - "content": " console.log(\"hello world\");", + "content": " console.log("hello world");", "lineAfter": 5, "type": "AddedLine", }, ], "context": "function hello() {", "fromFileRange": { - "lines": 4, + "lines": 1, "start": 4, }, "toFileRange": { diff --git a/src/__tests__/__snapshots__/consecutive-empty-files.test.ts.snap b/src/__tests__/__snapshots__/consecutive-empty-files.test.ts.snap new file mode 100644 index 0000000..c58a934 --- /dev/null +++ b/src/__tests__/__snapshots__/consecutive-empty-files.test.ts.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`consecutive-empty-files parse \`consecutive-empty-files\` 1`] = ` +{ + "files": [ + { + "chunks": [ + { + "changes": [ + { + "content": "content", + "lineAfter": 1, + "type": "AddedLine", + }, + { + "content": "No newline at end of file", + "type": "MessageLine", + }, + ], + "context": undefined, + "fromFileRange": { + "lines": 0, + "start": 0, + }, + "toFileRange": { + "lines": 1, + "start": 1, + }, + "type": "Chunk", + }, + ], + "path": "content", + "type": "AddedFile", + }, + { + "chunks": [], + "path": "empty", + "type": "AddedFile", + }, + { + "chunks": [], + "path": "empty2", + "type": "AddedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/__snapshots__/deleted-empty-file.test.ts.snap b/src/__tests__/__snapshots__/deleted-empty-file.test.ts.snap new file mode 100644 index 0000000..59b07a9 --- /dev/null +++ b/src/__tests__/__snapshots__/deleted-empty-file.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deleted-empty-file parse \`deleted-empty-file\` 1`] = ` +{ + "files": [ + { + "chunks": [], + "path": "src/empty", + "type": "DeletedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/__snapshots__/renamed-empty.test.ts.snap b/src/__tests__/__snapshots__/renamed-empty.test.ts.snap new file mode 100644 index 0000000..3a32eef --- /dev/null +++ b/src/__tests__/__snapshots__/renamed-empty.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renamed-empty parse \`renamed-empty\` 1`] = ` +{ + "files": [ + { + "chunks": [], + "pathAfter": "src/renamed-empty", + "pathBefore": "src/empty", + "type": "RenamedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/__snapshots__/unified.test.ts.snap b/src/__tests__/__snapshots__/unified.test.ts.snap new file mode 100644 index 0000000..ffe28b1 --- /dev/null +++ b/src/__tests__/__snapshots__/unified.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`unified parse \`unified 1`] = ` +{ + "files": [ + { + "chunks": [ + { + "changes": [ + { + "content": "ddd", + "lineAfter": 3, + "type": "AddedLine", + }, + ], + "context": "bbb", + "fromFileRange": { + "lines": 0, + "start": 2, + }, + "toFileRange": { + "lines": 1, + "start": 3, + }, + "type": "Chunk", + }, + ], + "path": "test.txt", + "type": "ChangedFile", + }, + ], + "type": "GitDiff", +} +`; diff --git a/src/__tests__/added-empty-file.test.ts b/src/__tests__/added-empty-file.test.ts new file mode 100644 index 0000000..a4d3dd0 --- /dev/null +++ b/src/__tests__/added-empty-file.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('added-empty-file', () => { + const fixture = getFixture('added-empty-file'); + + it('parse `added-empty-file`', () => { + expect(parseGitDiff(fixture)).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/consecutive-empty-files.test.ts b/src/__tests__/consecutive-empty-files.test.ts new file mode 100644 index 0000000..b862d96 --- /dev/null +++ b/src/__tests__/consecutive-empty-files.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('consecutive-empty-files', () => { + const fixture = getFixture('consecutive-empty-files'); + + it('parse `consecutive-empty-files`', () => { + expect(parseGitDiff(fixture)).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/deleted-empty-file.test.ts b/src/__tests__/deleted-empty-file.test.ts new file mode 100644 index 0000000..d313986 --- /dev/null +++ b/src/__tests__/deleted-empty-file.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('deleted-empty-file', () => { + const fixture = getFixture('deleted-empty-file'); + + it('parse `deleted-empty-file`', () => { + expect(parseGitDiff(fixture)).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/renamed-empty.test.ts b/src/__tests__/renamed-empty.test.ts new file mode 100644 index 0000000..9700b3a --- /dev/null +++ b/src/__tests__/renamed-empty.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('renamed-empty', () => { + const fixture = getFixture('renamed-empty'); + + it('parse `renamed-empty`', () => { + expect(parseGitDiff(fixture)).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/unified.test.ts b/src/__tests__/unified.test.ts new file mode 100644 index 0000000..87e5c54 --- /dev/null +++ b/src/__tests__/unified.test.ts @@ -0,0 +1,10 @@ +import { getFixture } from './test-utils'; +import parseGitDiff from '../parse-git-diff'; + +describe('unified', () => { + const fixture = getFixture('unified'); + + it('parse `unified', () => { + expect(parseGitDiff(fixture)).toMatchSnapshot(); + }); +}); diff --git a/src/parse-git-diff.ts b/src/parse-git-diff.ts index 4c74fce..31e6849 100644 --- a/src/parse-git-diff.ts +++ b/src/parse-git-diff.ts @@ -3,11 +3,8 @@ import type { GitDiff, AnyFileChange, AnyLineChange, - Chunk, ChunkRange, - CombinedChunk, AnyChunk, - FilledGitDiffOptions, GitDiffOptions, } from './types.js'; import { @@ -46,8 +43,7 @@ function parseFileChange(ctx: Context): AnyFileChange | undefined { if (!isComparisonInputLine(ctx.getCurLine())) { return; } - ctx.nextLine(); - + const comparisonLineParsed = parseComparisonInputLine(ctx); let isDeleted = false; let isNew = false; let isRename = false; @@ -58,8 +54,14 @@ function parseFileChange(ctx: Context): AnyFileChange | undefined { if (!extHeader) { break; } - if (extHeader.type === ExtendedHeader.Deleted) isDeleted = true; - if (extHeader.type === ExtendedHeader.NewFile) isNew = true; + if (extHeader.type === ExtendedHeader.Deleted) { + isDeleted = true; + pathBefore = comparisonLineParsed?.from || ''; + } + if (extHeader.type === ExtendedHeader.NewFile) { + isNew = true; + pathAfter = comparisonLineParsed?.to || ''; + } if (extHeader.type === ExtendedHeader.RenameFrom) { isRename = true; pathBefore = extHeader.path as string; @@ -73,33 +75,30 @@ function parseFileChange(ctx: Context): AnyFileChange | undefined { const changeMarkers = parseChangeMarkers(ctx); const chunks = parseChunks(ctx); - if (isDeleted && changeMarkers) { + if (isDeleted && chunks.length && chunks[0].type === 'BinaryFilesChunk') { return { type: FileType.Deleted, chunks, - path: changeMarkers.deleted, + path: chunks[0].pathBefore, }; - } else if ( - isDeleted && - chunks.length && - chunks[0].type === 'BinaryFilesChunk' - ) { + } + if (isDeleted) { return { type: FileType.Deleted, chunks, - path: chunks[0].pathBefore, + path: changeMarkers?.deleted || pathBefore, }; - } else if (isNew && changeMarkers) { + } else if (isNew && chunks.length && chunks[0].type === 'BinaryFilesChunk') { return { type: FileType.Added, chunks, - path: changeMarkers.added, + path: chunks[0].pathAfter, }; - } else if (isNew && chunks.length && chunks[0].type === 'BinaryFilesChunk') { + } else if (isNew) { return { type: FileType.Added, chunks, - path: chunks[0].pathAfter, + path: changeMarkers?.added || pathAfter, }; } else if (isRename) { return { @@ -125,7 +124,6 @@ function parseFileChange(ctx: Context): AnyFileChange | undefined { path: chunks[0].pathAfter, }; } - return; } @@ -133,6 +131,21 @@ function isComparisonInputLine(line: string): boolean { return line.indexOf('diff') === 0; } +function parseComparisonInputLine( + ctx: Context +): { from: string; to: string } | null { + const line = ctx.getCurLine(); + const [to, from] = line.split(' ').reverse(); + ctx.nextLine(); + if (to && from) { + return { + from: getFilePath(ctx, from, 'src'), + to: getFilePath(ctx, to, 'dst'), + }; + } + return null; +} + function parseChunks(context: Context): AnyChunk[] { const chunks: AnyChunk[] = []; @@ -151,7 +164,6 @@ function parseChunk(context: Context): AnyChunk | undefined { if (!chunkHeader) { return; } - if (chunkHeader.type === 'Normal') { const changes = parseChanges( context, @@ -195,7 +207,7 @@ function parseChunk(context: Context): AnyChunk | undefined { function parseExtendedHeader(ctx: Context) { if (isComparisonInputLine(ctx.getCurLine())) { - ctx.nextLine(); + return null; } const line = ctx.getCurLine(); const type = ExtendedHeaderValues.find((v) => line.startsWith(v)); @@ -264,7 +276,6 @@ function parseChunkHeader(ctx: Context) { toFileRange: getRange(addStart, addLines), } as const; } - const [all, delStart, delLines, addStart, addLines, context] = normalChunkExec; ctx.nextLine(); @@ -280,7 +291,7 @@ function getRange(start: string, lines?: string) { const startNum = parseInt(start, 10); return { start: startNum, - lines: lines === undefined ? startNum : parseInt(lines, 10), + lines: lines === undefined ? 1 : parseInt(lines, 10), }; } @@ -386,4 +397,5 @@ function getFilePath(ctx: Context, input: string, type: 'src' | 'dst') { } if (type === 'src') return input.replace(/^a\//, ''); if (type === 'dst') return input.replace(/^b\//, ''); + throw new Error('Unexpected unreachable code'); }