diff --git a/README.md b/README.md index 806b054..c2abb34 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,56 @@ import { findWorkspaceDir } from "pkg-types"; const workspaceDir = await findWorkspaceDir("."); ``` +### `resolveGitConfig` + +Finds closest `.git/config` file. + +```js +import { resolveGitConfig } from "pkg-types"; + +const gitConfig = await resolveGitConfig(".") +``` + +### `readGitConfig` + +Finds and reads closest `.git/config` file into a JS object. + +```js +import { resolveGitConfig } from "pkg-types"; + +const gitConfig = await readGitConfig(".") +``` + +### `writeGitConfig` + +Stringifies git config object into INI text format and writes it to a file. + +```js +import { writeGitConfig } from "pkg-types"; + +const gitConfig = await writeGitConfig(".git/config", gitConfig) +``` + +### `parseGitConfig` + +Parses a git config file in INI text format into a JavaScript object. + +```js +import { parseGitConfig } from "pkg-types"; + +const gitConfig = parseGitConfig(".") +``` + +### `stringifyGitConfig` + +Stringifies a git config object into a git config file INI text format. + +```js +import { parseGitConfig } from "pkg-types"; + +const stringifyGitConfig = stringifyGitConfig(".") +``` + ## Types **Note:** In order to make types working, you need to install `typescript` as a devDependency. @@ -132,7 +182,7 @@ const workspaceDir = await findWorkspaceDir("."); You can directly use typed interfaces: ```ts -import type { TSConfig, PackageJSON } from "pkg-types"; +import type { TSConfig, PackageJSON, GitConfig } from "pkg-types"; ``` You can also use define utils for type support for using in plain `.js` files and auto-complete in IDE. @@ -149,6 +199,12 @@ import type { defineTSConfig } from 'pkg-types' const pkg = defineTSConfig({}) ``` +```js +import type { defineGitConfig } from 'pkg-types' + +const gitConfig = defineGitConfig({}) +``` + ## Alternatives - [dominikg/tsconfck](https://github.com/dominikg/tsconfck) diff --git a/src/gitconfig/types.ts b/src/gitconfig/types.ts new file mode 100644 index 0000000..5745475 --- /dev/null +++ b/src/gitconfig/types.ts @@ -0,0 +1,32 @@ +export interface GitRemote { + [key: string]: unknown; + name?: string; + url?: string; + fetch?: string; +} + +export interface GitBranch { + [key: string]: unknown; + remote?: string; + merge?: string; + description?: string; + rebase?: boolean; +} + +export interface GitCoreConfig { + [key: string]: unknown; +} + +export interface GitConfigUser { + [key: string]: unknown; + name?: string; + email?: string; +} + +export interface GitConfig { + [key: string]: unknown; + core?: GitCoreConfig; + user?: GitConfigUser; + remote?: Record; + branch?: Record; +} diff --git a/src/gitconfig/utils.ts b/src/gitconfig/utils.ts new file mode 100644 index 0000000..b91983a --- /dev/null +++ b/src/gitconfig/utils.ts @@ -0,0 +1,52 @@ +import type { GitConfig } from "./types"; +import { readFile, writeFile } from "node:fs/promises"; +import { findNearestFile } from "../resolve/utils"; +import { parseINI, stringifyINI } from "confbox/ini"; +import type { ResolveOptions } from "../resolve/types"; + +/** + * Defines a git config object. + */ +export function defineGitConfig(config: GitConfig): GitConfig { + return config; +} + +/** + * Finds closest `.git/config` file. + */ +export async function resolveGitConfig( + dir: string, + opts?: ResolveOptions, +): Promise { + return findNearestFile(".git/config", { ...opts, startingFrom: dir }); +} + +/** + * Finds and reads closest `.git/config` file into a JS object. + */ +export async function readGitConfig(dir: string, opts?: ResolveOptions) { + const path = await resolveGitConfig(dir, opts); + const ini = await readFile(path, "utf8"); + return parseGitConfig(ini); +} + +/** + * Stringifies git config object into INI text format and writes it to a file. + */ +export async function writeGitConfig(path: string, config: GitConfig) { + await writeFile(path, stringifyGitConfig(config)); +} + +/** + * Parses a git config file in INI text format into a JavaScript object. + */ +export function parseGitConfig(ini: string): GitConfig { + return parseINI(ini.replaceAll(/^\[(\w+) "(.+)"\]$/gm, "[$1.$2]")); +} + +/** + * Stringifies a git config object into a git config file INI text format. + */ +export function stringifyGitConfig(config: GitConfig): string { + return stringifyINI(config).replaceAll(/^\[(\w+)\.(\w+)\]$/gm, '[$1 "$2"]'); +} diff --git a/src/index.ts b/src/index.ts index 3c3fd53..c3c1f6d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,3 +35,22 @@ export { resolveLockfile, findWorkspaceDir, } from "./packagejson/utils"; + +// --- git config --- + +export type { + GitConfig, + GitBranch, + GitCoreConfig, + GitRemote, + GitConfigUser, +} from "./gitconfig/types"; + +export { + defineGitConfig, + readGitConfig, + writeGitConfig, + resolveGitConfig, + parseGitConfig, + stringifyGitConfig, +} from "./gitconfig/utils"; diff --git a/test/fixture/_git/config b/test/fixture/_git/config new file mode 100644 index 0000000..49b4f78 --- /dev/null +++ b/test/fixture/_git/config @@ -0,0 +1,19 @@ +[core] +repositoryformatversion = 0 +filemode = true +bare = false +logallrefupdates = true +ignorecase = true +precomposeunicode = true + +[branch "main"] +remote = origin +merge = refs/heads/main + +[branch "develop"] +remote = origin +merge = refs/heads/develop + +[remote "origin"] +url = https://github.com/username/repo.git +fetch = +refs/heads/*:refs/remotes/origin/* diff --git a/test/index.test.ts b/test/index.test.ts index ce6c9cd..5abad54 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,7 @@ import { fileURLToPath } from "node:url"; -import { readFile } from "node:fs/promises"; +import { cp, readFile, rm } from "node:fs/promises"; import { dirname, resolve } from "pathe"; -import { describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { expectTypeOf } from "expect-type"; import { type TSConfig, @@ -14,6 +14,11 @@ import { writeTSConfig, resolveLockfile, findWorkspaceDir, + // gitconfig + resolveGitConfig, + readGitConfig, + writeGitConfig, + parseGitConfig, } from "../src"; const fixtureDir = resolve(dirname(fileURLToPath(import.meta.url)), "fixture"); @@ -179,3 +184,64 @@ describe("findWorkspaceDir", () => { ); }); }); + +describe(".git/config", () => { + beforeAll(async () => { + await rm(rFixture(".git"), { force: true, recursive: true }); + await cp(rFixture("_git"), rFixture(".git"), { + recursive: true, + }); + }); + + afterAll(async () => { + await rm(rFixture(".git"), { force: true, recursive: true }); + }); + + it("resolveGitConfig", async () => { + expect(await resolveGitConfig(rFixture("."))).to.equal( + rFixture(".git/config"), + ); + }); + + it("readGitConfig", async () => { + expect(await readGitConfig(rFixture("."))).toMatchObject({ + core: { + bare: false, + filemode: true, + ignorecase: true, + logallrefupdates: true, + precomposeunicode: true, + repositoryformatversion: "0", + }, + branch: { + develop: { + merge: "refs/heads/develop", + remote: "origin", + }, + main: { + merge: "refs/heads/main", + remote: "origin", + }, + }, + remote: { + origin: { + fetch: "+refs/heads/*:refs/remotes/origin/*", + url: "https://github.com/username/repo.git", + }, + }, + }); + }); + + it("writeGitConfig", async () => { + const fixtureConfigINI = await readFile(rFixture(".git/config"), "utf8"); + + await writeGitConfig( + rFixture(".git/config.tmp"), + parseGitConfig(fixtureConfigINI), + ); + + const newConfigINI = await readFile(rFixture(".git/config.tmp"), "utf8"); + + expect(newConfigINI.trim()).toBe(fixtureConfigINI.trim()); + }); +});