Skip to content

Commit de78b01

Browse files
feat(log): add filepath, force, follow params
1 parent 664b301 commit de78b01

File tree

9 files changed

+2371
-24
lines changed

9 files changed

+2371
-24
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "git-essentials",
33
"version": "0.0.0-development",
44
"description": "A collection of essential Git commands for your browser and Node.js",
5+
"type": "module",
56
"main": "dist/esm/index.js",
67
"types": "index.d.ts",
78
"typesVersions": {

scripts/fix-build.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
const path = require('path')
2-
const fs = require('fs')
3-
1+
import fs from 'fs'
2+
import path from 'path'
43

54
// Delete empty TypeScript declarations
65
deleteEmptyTypeScriptDeclarations('dist/types')

src/api/log.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { _log } from '../commands/log'
1+
import { Cache } from '../models/Cache'
22
import { FileSystem } from '../models/FileSystem'
33
import { FsClient } from '../models/FsClient'
4-
import { Cache } from '../models/Cache'
4+
import { ReadCommitResult } from '../api/readCommit'
5+
import { _log } from '../commands/log'
56
import { assertParameter } from '../utils/assertParameter'
67
import { join } from '../utils/join'
7-
import { ReadCommitResult } from '../api/readCommit'
8-
98

109
export type LogParams = {
1110
/** A file system client. */
@@ -26,6 +25,15 @@ export type LogParams = {
2625
/** Return history newer than the given date. Can be combined with `depth` to get whichever is shorter. */
2726
since?: Date
2827

28+
/** Get the commit for the filepath only. */
29+
filepath?: string
30+
31+
/** Do not throw error if filepath does not exist (works only for a single file, default: `false`). */
32+
force?: boolean
33+
34+
/** Continue listing the history of a file beyond renames (works only for a single file, default: `false`). */
35+
follow?: boolean
36+
2937
/** A cache object. */
3038
cache?: Cache
3139
}
@@ -55,6 +63,9 @@ export async function log({
5563
ref = 'HEAD',
5664
depth,
5765
since, // Date
66+
filepath,
67+
force = false,
68+
follow = false,
5869
cache = {},
5970
}: LogParams): Promise<Array<ReadCommitResult>> {
6071
try {
@@ -69,6 +80,9 @@ export async function log({
6980
ref,
7081
depth,
7182
since,
83+
filepath,
84+
force,
85+
follow
7286
})
7387
} catch (err: any) {
7488
err.caller = 'git.log'

src/commands/log.ts

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { FileSystem } from '../models/FileSystem'
2-
import { Cache } from '../models/Cache'
31
import { ReadCommitResult, _readCommit } from '../commands/readCommit'
2+
3+
import { Cache } from '../models/Cache'
4+
import { FileSystem } from '../models/FileSystem'
45
import { GitRefManager } from '../managers/GitRefManager'
56
import { GitShallowManager } from '../managers/GitShallowManager'
7+
import { NotFoundError } from '../errors'
68
import { compareAge } from '../utils/compareAge'
9+
import { resolveFileIdInTree } from '../utils/resolveFileIdInTree'
10+
import { resolveFilepath } from '../utils/resolveFilepath'
711

812
type LogParams = {
913
fs: FileSystem
@@ -12,24 +16,44 @@ type LogParams = {
1216
ref: string
1317
depth?: number
1418
since?: Date
19+
filepath?: string
20+
force?: boolean,
21+
follow?: boolean,
1522
}
1623

1724
/**
1825
* Get commit descriptions from the git history.
1926
*
2027
* @internal
2128
*/
22-
export async function _log({ fs, cache, gitdir, ref, depth, since }: LogParams): Promise<Array<ReadCommitResult>> {
29+
export async function _log({
30+
fs,
31+
cache,
32+
gitdir,
33+
ref,
34+
depth,
35+
since,
36+
filepath,
37+
force,
38+
follow
39+
}: LogParams): Promise<Array<ReadCommitResult>> {
2340
const sinceTimestamp =
2441
typeof since === 'undefined'
2542
? undefined
2643
: Math.floor(since.valueOf() / 1000)
2744
// TODO: In the future, we may want to have an API where we return a
2845
// async iterator that emits commits.
29-
const commits = []
46+
const commits: ReadCommitResult[] = []
3047
const shallowCommits = await GitShallowManager.read({ fs, gitdir })
3148
const oid = await GitRefManager.resolve({ fs, gitdir, ref })
3249
const tips = [await _readCommit({ fs, cache, gitdir, oid })]
50+
let lastFileOid: string | undefined = undefined
51+
let lastCommit: ReadCommitResult | undefined = undefined
52+
let isOk: boolean | undefined = undefined
53+
54+
function endCommit(commit: ReadCommitResult) {
55+
if (isOk && filepath) commits.push(commit)
56+
}
3357

3458
while (tips.length > 0) {
3559
const commit = tips.pop()!
@@ -42,10 +66,82 @@ export async function _log({ fs, cache, gitdir, ref, depth, since }: LogParams):
4266
break
4367
}
4468

45-
commits.push(commit)
69+
if (filepath) {
70+
let vFileOid
71+
try {
72+
vFileOid = await resolveFilepath({
73+
fs,
74+
cache,
75+
gitdir,
76+
oid: commit.commit.tree!,
77+
filepath,
78+
})
79+
if (lastCommit && lastFileOid !== vFileOid) {
80+
commits.push(lastCommit)
81+
}
82+
lastFileOid = vFileOid
83+
lastCommit = commit
84+
isOk = true
85+
} catch (e) {
86+
if (e instanceof NotFoundError) {
87+
let found: string | string[] | boolean | undefined = follow && lastFileOid
88+
if (found) {
89+
found = await resolveFileIdInTree({
90+
fs,
91+
cache,
92+
gitdir,
93+
oid: commit.commit.tree!,
94+
fileId: lastFileOid!,
95+
})
96+
if (found) {
97+
if (Array.isArray(found)) {
98+
if (lastCommit) {
99+
const lastFound = await resolveFileIdInTree({
100+
fs,
101+
cache,
102+
gitdir,
103+
oid: lastCommit.commit.tree!,
104+
fileId: lastFileOid!,
105+
})
106+
if (Array.isArray(lastFound)) {
107+
found = found.filter(p => lastFound.indexOf(p) === -1)
108+
if (found.length === 1) {
109+
found = found[0]
110+
filepath = found
111+
if (lastCommit) commits.push(lastCommit)
112+
} else {
113+
found = false
114+
if (lastCommit) commits.push(lastCommit)
115+
break
116+
}
117+
}
118+
}
119+
} else {
120+
filepath = found
121+
if (lastCommit) commits.push(lastCommit)
122+
}
123+
}
124+
}
125+
if (!found) {
126+
if (isOk && lastFileOid) {
127+
commits.push(lastCommit!)
128+
if (!force) break
129+
}
130+
if (!force && !follow) throw e
131+
}
132+
lastCommit = commit
133+
isOk = false
134+
} else throw e
135+
}
136+
} else {
137+
commits.push(commit)
138+
}
46139

47140
// Stop the loop if we have enough commits now.
48-
if (depth !== undefined && commits.length === depth) break
141+
if (depth !== undefined && commits.length === depth) {
142+
endCommit(commit)
143+
break
144+
}
49145

50146
// If this is not a shallow commit...
51147
if (!shallowCommits.has(commit.oid)) {
@@ -60,10 +156,13 @@ export async function _log({ fs, cache, gitdir, ref, depth, since }: LogParams):
60156
}
61157

62158
// Stop the loop if there are no more commit parents
63-
if (tips.length === 0) break
159+
if (tips.length === 0) {
160+
endCommit(commit)
161+
}
64162

65163
// Process tips in order by age
66164
tips.sort((a, b) => compareAge(a.commit, b.commit))
67165
}
166+
68167
return commits
69168
}

src/utils/resolveFileIdInTree.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Cache } from '../models/Cache'
2+
import { FileSystem } from '../models/FileSystem'
3+
import { GitTree } from '../models/GitTree'
4+
import { join } from './join'
5+
import { _readObject as readObject } from '../storage/readObject'
6+
import { resolveTree } from './resolveTree'
7+
8+
// the empty file content object id
9+
const EMPTY_OID = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
10+
11+
type ResolveFileIdInTreeParams = {
12+
fs: FileSystem
13+
cache: Cache
14+
gitdir: string
15+
oid: string
16+
fileId: string
17+
}
18+
19+
type ResolveFileIdParams = {
20+
fs: FileSystem
21+
cache: Cache
22+
gitdir: string
23+
tree: GitTree
24+
fileId: string
25+
oid: string
26+
filepaths?: string[]
27+
parentPath?: string
28+
}
29+
30+
export async function resolveFileIdInTree({
31+
fs,
32+
cache,
33+
gitdir,
34+
oid,
35+
fileId
36+
}: ResolveFileIdInTreeParams): Promise<string | string[] | undefined> {
37+
if (fileId === EMPTY_OID) {
38+
return
39+
}
40+
41+
const _oid = oid
42+
const result = await resolveTree({ fs, cache, gitdir, oid })
43+
const tree = result.tree
44+
45+
if (fileId === result.oid && 'path' in result) {
46+
return (result as any).path
47+
} else {
48+
const filepaths = await _resolveFileId({
49+
fs,
50+
cache,
51+
gitdir,
52+
tree,
53+
fileId,
54+
oid: _oid,
55+
})
56+
57+
if (filepaths.length === 0) {
58+
return undefined
59+
} else if (filepaths.length === 1) {
60+
return filepaths[0]
61+
} else {
62+
return filepaths
63+
}
64+
}
65+
}
66+
67+
async function _resolveFileId({
68+
fs,
69+
cache,
70+
gitdir,
71+
tree,
72+
fileId,
73+
oid,
74+
filepaths = [],
75+
parentPath = '',
76+
}: ResolveFileIdParams): Promise<string[]> {
77+
const walks = tree.entries().map(function(entry) {
78+
let result: string | Promise<string | string[]> | undefined = undefined
79+
if (entry.oid === fileId) {
80+
result = join(parentPath, entry.path)
81+
filepaths.push(result)
82+
} else if (entry.type === 'tree') {
83+
result = readObject({
84+
fs,
85+
cache,
86+
gitdir,
87+
oid: entry.oid,
88+
}).then(function({ object }) {
89+
return _resolveFileId({
90+
fs,
91+
cache,
92+
gitdir,
93+
tree: GitTree.from(object),
94+
fileId,
95+
oid,
96+
filepaths,
97+
parentPath: join(parentPath, entry.path),
98+
})
99+
})
100+
}
101+
return result
102+
})
103+
104+
await Promise.all(walks)
105+
return filepaths
106+
}

src/utils/resolveFilepath.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { FileSystem } from '../models/FileSystem'
21
import { Cache } from '../models'
2+
import { FileSystem } from '../models/FileSystem'
3+
import { GitTree } from '../models/GitTree'
34
import { InvalidFilepathError } from '../errors/InvalidFilepathError'
45
import { NotFoundError } from '../errors/NotFoundError'
56
import { ObjectTypeError } from '../errors/ObjectTypeError'
6-
import { GitTree } from '../models/GitTree'
77
import { _readObject as readObject } from '../storage/readObject'
88
import { resolveTree } from '../utils/resolveTree'
99

src/utils/resolveTree.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { FileSystem } from '../models/FileSystem'
21
import { Cache } from '../models/Cache'
3-
import { ObjectTypeError } from '../errors'
2+
import { FileSystem } from '../models/FileSystem'
43
import { GitAnnotatedTag } from '../models/GitAnnotatedTag'
54
import { GitCommit } from '../models/GitCommit'
65
import { GitTree } from '../models/GitTree'
6+
import { ObjectTypeError } from '../errors'
77
import { _readObject } from '../storage/readObject'
88

9-
109
/** @internal */
1110
export async function resolveTree(
1211
{ fs, cache, gitdir, oid }:

0 commit comments

Comments
 (0)