Skip to content

Commit

Permalink
feat(log): add filepath, force, follow params
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-titarenko committed Feb 24, 2024
1 parent 664b301 commit de78b01
Show file tree
Hide file tree
Showing 9 changed files with 2,371 additions and 24 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "git-essentials",
"version": "0.0.0-development",
"description": "A collection of essential Git commands for your browser and Node.js",
"type": "module",
"main": "dist/esm/index.js",
"types": "index.d.ts",
"typesVersions": {
Expand Down
5 changes: 2 additions & 3 deletions scripts/fix-build.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const path = require('path')
const fs = require('fs')

import fs from 'fs'
import path from 'path'

// Delete empty TypeScript declarations
deleteEmptyTypeScriptDeclarations('dist/types')
Expand Down
22 changes: 18 additions & 4 deletions src/api/log.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { _log } from '../commands/log'
import { Cache } from '../models/Cache'
import { FileSystem } from '../models/FileSystem'
import { FsClient } from '../models/FsClient'
import { Cache } from '../models/Cache'
import { ReadCommitResult } from '../api/readCommit'
import { _log } from '../commands/log'
import { assertParameter } from '../utils/assertParameter'
import { join } from '../utils/join'
import { ReadCommitResult } from '../api/readCommit'


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

/** Get the commit for the filepath only. */
filepath?: string

/** Do not throw error if filepath does not exist (works only for a single file, default: `false`). */
force?: boolean

/** Continue listing the history of a file beyond renames (works only for a single file, default: `false`). */
follow?: boolean

/** A cache object. */
cache?: Cache
}
Expand Down Expand Up @@ -55,6 +63,9 @@ export async function log({
ref = 'HEAD',
depth,
since, // Date
filepath,
force = false,
follow = false,
cache = {},
}: LogParams): Promise<Array<ReadCommitResult>> {
try {
Expand All @@ -69,6 +80,9 @@ export async function log({
ref,
depth,
since,
filepath,
force,
follow
})
} catch (err: any) {
err.caller = 'git.log'
Expand Down
113 changes: 106 additions & 7 deletions src/commands/log.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { FileSystem } from '../models/FileSystem'
import { Cache } from '../models/Cache'
import { ReadCommitResult, _readCommit } from '../commands/readCommit'

import { Cache } from '../models/Cache'
import { FileSystem } from '../models/FileSystem'
import { GitRefManager } from '../managers/GitRefManager'
import { GitShallowManager } from '../managers/GitShallowManager'
import { NotFoundError } from '../errors'
import { compareAge } from '../utils/compareAge'
import { resolveFileIdInTree } from '../utils/resolveFileIdInTree'
import { resolveFilepath } from '../utils/resolveFilepath'

type LogParams = {
fs: FileSystem
Expand All @@ -12,24 +16,44 @@ type LogParams = {
ref: string
depth?: number
since?: Date
filepath?: string
force?: boolean,
follow?: boolean,
}

/**
* Get commit descriptions from the git history.
*
* @internal
*/
export async function _log({ fs, cache, gitdir, ref, depth, since }: LogParams): Promise<Array<ReadCommitResult>> {
export async function _log({
fs,
cache,
gitdir,
ref,
depth,
since,
filepath,
force,
follow
}: LogParams): Promise<Array<ReadCommitResult>> {
const sinceTimestamp =
typeof since === 'undefined'
? undefined
: Math.floor(since.valueOf() / 1000)
// TODO: In the future, we may want to have an API where we return a
// async iterator that emits commits.
const commits = []
const commits: ReadCommitResult[] = []
const shallowCommits = await GitShallowManager.read({ fs, gitdir })
const oid = await GitRefManager.resolve({ fs, gitdir, ref })
const tips = [await _readCommit({ fs, cache, gitdir, oid })]
let lastFileOid: string | undefined = undefined
let lastCommit: ReadCommitResult | undefined = undefined
let isOk: boolean | undefined = undefined

function endCommit(commit: ReadCommitResult) {
if (isOk && filepath) commits.push(commit)
}

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

commits.push(commit)
if (filepath) {
let vFileOid
try {
vFileOid = await resolveFilepath({
fs,
cache,
gitdir,
oid: commit.commit.tree!,
filepath,
})
if (lastCommit && lastFileOid !== vFileOid) {
commits.push(lastCommit)
}
lastFileOid = vFileOid
lastCommit = commit
isOk = true
} catch (e) {
if (e instanceof NotFoundError) {
let found: string | string[] | boolean | undefined = follow && lastFileOid
if (found) {
found = await resolveFileIdInTree({
fs,
cache,
gitdir,
oid: commit.commit.tree!,
fileId: lastFileOid!,
})
if (found) {
if (Array.isArray(found)) {
if (lastCommit) {
const lastFound = await resolveFileIdInTree({
fs,
cache,
gitdir,
oid: lastCommit.commit.tree!,
fileId: lastFileOid!,
})
if (Array.isArray(lastFound)) {
found = found.filter(p => lastFound.indexOf(p) === -1)
if (found.length === 1) {
found = found[0]
filepath = found
if (lastCommit) commits.push(lastCommit)
} else {
found = false
if (lastCommit) commits.push(lastCommit)
break
}
}
}
} else {
filepath = found
if (lastCommit) commits.push(lastCommit)
}
}
}
if (!found) {
if (isOk && lastFileOid) {
commits.push(lastCommit!)
if (!force) break
}
if (!force && !follow) throw e
}
lastCommit = commit
isOk = false
} else throw e
}
} else {
commits.push(commit)
}

// Stop the loop if we have enough commits now.
if (depth !== undefined && commits.length === depth) break
if (depth !== undefined && commits.length === depth) {
endCommit(commit)
break
}

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

// Stop the loop if there are no more commit parents
if (tips.length === 0) break
if (tips.length === 0) {
endCommit(commit)
}

// Process tips in order by age
tips.sort((a, b) => compareAge(a.commit, b.commit))
}

return commits
}
106 changes: 106 additions & 0 deletions src/utils/resolveFileIdInTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Cache } from '../models/Cache'
import { FileSystem } from '../models/FileSystem'
import { GitTree } from '../models/GitTree'
import { join } from './join'
import { _readObject as readObject } from '../storage/readObject'
import { resolveTree } from './resolveTree'

// the empty file content object id
const EMPTY_OID = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'

type ResolveFileIdInTreeParams = {
fs: FileSystem
cache: Cache
gitdir: string
oid: string
fileId: string
}

type ResolveFileIdParams = {
fs: FileSystem
cache: Cache
gitdir: string
tree: GitTree
fileId: string
oid: string
filepaths?: string[]
parentPath?: string
}

export async function resolveFileIdInTree({
fs,
cache,
gitdir,
oid,
fileId
}: ResolveFileIdInTreeParams): Promise<string | string[] | undefined> {
if (fileId === EMPTY_OID) {
return
}

const _oid = oid
const result = await resolveTree({ fs, cache, gitdir, oid })
const tree = result.tree

if (fileId === result.oid && 'path' in result) {
return (result as any).path
} else {
const filepaths = await _resolveFileId({
fs,
cache,
gitdir,
tree,
fileId,
oid: _oid,
})

if (filepaths.length === 0) {
return undefined
} else if (filepaths.length === 1) {
return filepaths[0]
} else {
return filepaths
}
}
}

async function _resolveFileId({
fs,
cache,
gitdir,
tree,
fileId,
oid,
filepaths = [],
parentPath = '',
}: ResolveFileIdParams): Promise<string[]> {
const walks = tree.entries().map(function(entry) {
let result: string | Promise<string | string[]> | undefined = undefined
if (entry.oid === fileId) {
result = join(parentPath, entry.path)
filepaths.push(result)
} else if (entry.type === 'tree') {
result = readObject({
fs,
cache,
gitdir,
oid: entry.oid,
}).then(function({ object }) {
return _resolveFileId({
fs,
cache,
gitdir,
tree: GitTree.from(object),
fileId,
oid,
filepaths,
parentPath: join(parentPath, entry.path),
})
})
}
return result
})

await Promise.all(walks)
return filepaths
}
4 changes: 2 additions & 2 deletions src/utils/resolveFilepath.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FileSystem } from '../models/FileSystem'
import { Cache } from '../models'
import { FileSystem } from '../models/FileSystem'
import { GitTree } from '../models/GitTree'
import { InvalidFilepathError } from '../errors/InvalidFilepathError'
import { NotFoundError } from '../errors/NotFoundError'
import { ObjectTypeError } from '../errors/ObjectTypeError'
import { GitTree } from '../models/GitTree'
import { _readObject as readObject } from '../storage/readObject'
import { resolveTree } from '../utils/resolveTree'

Expand Down
5 changes: 2 additions & 3 deletions src/utils/resolveTree.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { FileSystem } from '../models/FileSystem'
import { Cache } from '../models/Cache'
import { ObjectTypeError } from '../errors'
import { FileSystem } from '../models/FileSystem'
import { GitAnnotatedTag } from '../models/GitAnnotatedTag'
import { GitCommit } from '../models/GitCommit'
import { GitTree } from '../models/GitTree'
import { ObjectTypeError } from '../errors'
import { _readObject } from '../storage/readObject'


/** @internal */
export async function resolveTree(
{ fs, cache, gitdir, oid }:
Expand Down
Loading

0 comments on commit de78b01

Please sign in to comment.