Skip to content

Commit

Permalink
feat: git config utils (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Mar 3, 2025
1 parent 8a0e515 commit 07b09ee
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 3 deletions.
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,64 @@ 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.

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.
Expand All @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions src/gitconfig/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, GitRemote>;
branch?: Record<string, GitBranch>;
}
52 changes: 52 additions & 0 deletions src/gitconfig/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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"]');
}
19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
19 changes: 19 additions & 0 deletions test/fixture/_git/config
Original file line number Diff line number Diff line change
@@ -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/*
70 changes: 68 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -14,6 +14,11 @@ import {
writeTSConfig,
resolveLockfile,
findWorkspaceDir,
// gitconfig
resolveGitConfig,
readGitConfig,
writeGitConfig,
parseGitConfig,
} from "../src";

const fixtureDir = resolve(dirname(fileURLToPath(import.meta.url)), "fixture");
Expand Down Expand Up @@ -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());
});
});

0 comments on commit 07b09ee

Please sign in to comment.