Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: git config utils #217

Merged
merged 5 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
});
});