diff --git a/README.md b/README.md index 4eab2bb..1a19912 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,10 @@ Show only commits in the specified branch or revision range. By default uses the current branch and defaults to `HEAD` (i.e. the whole history leading to the current commit). +### fileLineRange + +Optional field for getting only the commits that affected a specific line range of a given file. + ### file Optional file filter for the `git log` command diff --git a/src/index.ts b/src/index.ts index 6345e62..724afa2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,20 @@ export type CommitField = keyof typeof fieldMap; const notOptFields = ["status", "files"] as const; type NotOptField = typeof notOptFields[number]; +export interface FileLineRange { + /** Will be pass as -L ,: */ + + /** The file to get the commits for */ + file: string; + /** The number of the first line in the desired range */ + startLine: number; + /** + * Either the absolute line number for the end of the desired range, + * or the offset from the startLine + */ + endLine: number | string; +} + const defaultFields = [ "abbrevHash", "hash", @@ -82,6 +96,8 @@ export interface GitlogOptions { * the whole history leading to the current commit). */ branch?: string; + /** Range of lines for a given file to find the commits for */ + fileLineRange?: FileLineRange; /** File filter for the git log command */ file?: string; /** Limit the commits output to ones with author header lines that match the specified pattern. */ @@ -263,10 +279,14 @@ function createCommand( } // File and file status - if (options.nameStatus) { + if (options.nameStatus && !options.fileLineRange) { command += " --name-status"; } + if (options.fileLineRange) { + command += ` -L ${options.fileLineRange.startLine},${options.fileLineRange.endLine}:${options.fileLineRange.file}`; + } + if (options.file) { command += ` -- ${options.file}`; } diff --git a/test/create-repo.sh b/test/create-repo.sh index c408e22..465f853 100755 --- a/test/create-repo.sh +++ b/test/create-repo.sh @@ -58,6 +58,23 @@ git checkout master git merge --no-edit --no-ff new-merge-branch git branch -d new-merge-branch +# Modify specific lines +touch fileToModify +# Add contents large enough to break up diffs +for i in {1..20} + do + printf "$i\n" >> fileToModify + + done +git add fileToModify +git commit -m "added long content file" +sed -i '' -e 's/2/4/g' ./fileToModify +git add fileToModify +git commit -m "Modify multiple parts of the file and come close to, but not the first line" +sed -i '' -e 's/40/44/' ./fileToModify +git add fileToModify +git commit -m "Modify end of the file" + # git symbolic-ref HEAD refs/heads/test-branch # rm .git/index # git clen -fdx diff --git a/test/gitlog.test.ts b/test/gitlog.test.ts index d78156c..8d8e142 100644 --- a/test/gitlog.test.ts +++ b/test/gitlog.test.ts @@ -58,23 +58,23 @@ describe("gitlog", () => { ); }); - it("returns 22 commits from repository with all=false", (done) => { + it("returns 25 commits from repository with all=false", (done) => { gitlog( { repo: testRepoLocation, all: false, number: 100 }, (err, commits) => { expect(err).toBeNull(); - expect(commits.length).toBe(22); + expect(commits.length).toBe(25); done(); } ); }); - it("returns 23 commits from repository with all=true", (done) => { + it("returns 26 commits from repository with all=true", (done) => { gitlog( { repo: testRepoLocation, all: true, number: 100 }, (err, commits) => { expect(err).toBeNull(); - expect(commits.length).toBe(23); + expect(commits.length).toBe(26); done(); } ); @@ -153,17 +153,6 @@ describe("gitlog", () => { expect(commits[0].files).not.toBeDefined(); }); - it("returns nameStatus fields", () => { - const commits = gitlog({ repo: testRepoLocation }); - - expect(commits[0].abbrevHash).toBeDefined(); - expect(commits[0].subject).toBeDefined(); - expect(commits[0].authorName).toBeDefined(); - expect(commits[0].hash).toBeDefined(); - expect(commits[0].status).toBeDefined(); - expect(commits[0].files).toBeDefined(); - }); - it('returns fields with "since" limit', () => { const commits = gitlog({ repo: testRepoLocation, since: "1 minutes ago" }); expect(commits).toHaveLength(10); @@ -237,14 +226,9 @@ describe("gitlog", () => { }); }); - it("returns A status for files that are added", () => { - const commits = gitlog({ repo: testRepoLocation }); - expect(commits[1].status[0]).toBe("A"); - }); - it("returns C100 status for files that are copied", () => { const commits = gitlog({ repo: testRepoLocation, findCopiesHarder: true }); - expect(commits[1].status[0]).toBe("C100"); + expect(commits[4].status[0]).toBe("C100"); }); it("returns merge commits files when includeMergeCommitFiles is true", () => { @@ -252,25 +236,41 @@ describe("gitlog", () => { repo: testRepoLocation, includeMergeCommitFiles: true, }); - expect(commits[0].files[0]).toBe("foo"); + expect(commits[3].files[0]).toBe("foo"); }); - it("returns M status for files that are modified", () => { - const commits = gitlog({ repo: testRepoLocation }); - expect(commits[3].status[0]).toBe("M"); - }); + describe("Only repo option", () => { + let commits: any[]; + beforeAll(() => { + commits = gitlog({ repo: testRepoLocation }); + }); - it("returns D status for files that are deleted", () => { - const commits = gitlog({ repo: testRepoLocation }); - expect(commits[4].status[0]).toBe("D"); - }); + it("returns nameStatus fields", () => { + expect(commits[0].abbrevHash).toBeDefined(); + expect(commits[0].subject).toBeDefined(); + expect(commits[0].authorName).toBeDefined(); + expect(commits[0].hash).toBeDefined(); + expect(commits[0].status).toBeDefined(); + expect(commits[0].files).toBeDefined(); + }); - it("returns author name correctly", () => { - const commits = gitlog({ repo: testRepoLocation }); + it("returns A status for files that are added", () => { + expect(commits[4].status[0]).toBe("A"); + }); - expect.assertions(10); - commits.forEach((commit) => { - expect(commit.authorName).toBe("Your Name"); + it("returns M status for files that are modified", () => { + expect(commits[6].status[0]).toBe("M"); + }); + + it("returns D status for files that are deleted", () => { + expect(commits[7].status[0]).toBe("D"); + }); + + it("returns author name correctly", () => { + expect.assertions(10); + commits.forEach((commit) => { + expect(commit.authorName).toBe("Your Name"); + }); }); }); @@ -299,6 +299,27 @@ describe("gitlog", () => { expect(commits[0].rawBody).toBeDefined(); }); + it("should be able to get commit counts for a specific line only", () => { + const commitsForFirstLine = gitlog({ + repo: testRepoLocation, + fileLineRange: { + file: "fileToModify", + startLine: 1, + endLine: 1, + }, + }); + expect(commitsForFirstLine.length).toBe(1); + const commitsForLastLine = gitlog({ + repo: testRepoLocation, + fileLineRange: { + file: "fileToModify", + startLine: 20, + endLine: 20, + }, + }); + expect(commitsForLastLine.length).toBe(3); + }); + afterAll(() => { execInTestDir(`${__dirname}/delete-repo.sh`); });