Skip to content

Commit

Permalink
feat: git.getConfig (#666)
Browse files Browse the repository at this point in the history
* feat: `git.getConfig` add support for getting the current value of a git configuration setting based on its name.

```typescript
// read value from the combined settings git uses
expect(await git.getConfig('user.name'))
  .toHaveProperty('value', 'My Name');

// read value from a specific scope
expect(await git.getConfig('user.name', 'local'))
  .toHaveProperty('value', 'My Name In This Repo');
```

To allow for getting all potential values (across scopes) for a named property (equivalent to `git config --get-all`) the resulting `ConfigGetResult` instance's `values` array.
  • Loading branch information
steveukx authored Aug 14, 2021
1 parent 1f8d2a0 commit 5c9c660
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 13 deletions.
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ For type details of the response for each of the tasks, please see the [TypeScri
`true` the configuration setting is appended to rather than overwritten in the local config. Use the `scope` argument
to pick where to save the new configuration setting (use the exported `GitConfigScope` enum, or equivalent string
values - `worktree | local | global | system`).

- `.getConfig(key)` get the value(s) for a named key as a [ConfigGetResult](typings/response.d.ts)
- `.getConfig(key, scope)` get the value(s) for a named key as a [ConfigGetResult](typings/response.d.ts) but limit the
scope of the properties searched to a single specified scope (use the exported `GitConfigScope` enum, or equivalent
string values - `worktree | local | global | system`)

- `.listConfig()` reads the current configuration and returns a [ConfigListSummary](./src/lib/responses/ConfigList.ts)
- `.listConfig(scope: GitConfigScope)` as with `listConfig` but returns only those items in a specified scope (note that configuration values are overlaid on top of each other to build the config `git` will actually use - to resolve the configuration you are using use `(await listConfig()).all` without the scope argument)
Expand Down
49 changes: 42 additions & 7 deletions src/lib/responses/ConfigList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigListSummary, ConfigValues } from '../../../typings';
import { ConfigGetResult, ConfigListSummary, ConfigValues } from '../../../typings';
import { last, splitOn } from '../utils';

export class ConfigList implements ConfigListSummary {
Expand Down Expand Up @@ -47,18 +47,53 @@ export class ConfigList implements ConfigListSummary {

export function configListParser(text: string): ConfigList {
const config = new ConfigList();
const lines = text.split('\0');

for (let i = 0, max = lines.length - 1; i < max;) {
const file = configFilePath(lines[i++]);
const [key, value] = splitOn(lines[i++], '\n');

config.addValue(file, key, value);
for (const item of configParser(text)) {
config.addValue(item.file, item.key, item.value);
}

return config;
}

export function configGetParser(text: string, key: string): ConfigGetResult {
let value: string | null = null;
const values: string[] = [];
const scopes: Map<string, string[]> = new Map();

for (const item of configParser(text)) {
if (item.key !== key) {
continue;
}

values.push(value = item.value);

if (!scopes.has(item.file)) {
scopes.set(item.file, []);
}

scopes.get(item.file)!.push(value);
}

return {
key,
paths: Array.from(scopes.keys()),
scopes,
value,
values
};
}

function configFilePath(filePath: string): string {
return filePath.replace(/^(file):/, '');
}

function* configParser(text: string) {
const lines = text.split('\0');

for (let i = 0, max = lines.length - 1; i < max;) {
const file = configFilePath(lines[i++]);
const [key, value] = splitOn(lines[i++], '\n');

yield {file, key, value};
}
}
31 changes: 27 additions & 4 deletions src/lib/tasks/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConfigListSummary, SimpleGit } from '../../../typings';
import { configListParser } from '../responses/ConfigList';
import { ConfigGetResult, ConfigListSummary, SimpleGit } from '../../../typings';
import { configGetParser, configListParser } from '../responses/ConfigList';
import { SimpleGitApi } from '../simple-git-api';
import { StringTask } from '../types';
import { trailingFunctionArgument } from '../utils';
Expand Down Expand Up @@ -36,6 +36,22 @@ function addConfigTask(key: string, value: string, append: boolean, scope: GitCo
}
}

function getConfigTask(key: string, scope?: GitConfigScope): StringTask<ConfigGetResult> {
const commands: string[] = ['config', '--null', '--show-origin', '--get-all', key];

if (scope) {
commands.splice(1, 0, `--${scope}`);
}

return {
commands,
format: 'utf-8',
parser(text) {
return configGetParser(text, key);
}
};
}

function listConfigTask(scope?: GitConfigScope): StringTask<ConfigListSummary> {
const commands = ['config', '--list', '--show-origin', '--null'];

Expand All @@ -46,13 +62,13 @@ function listConfigTask(scope?: GitConfigScope): StringTask<ConfigListSummary> {
return {
commands,
format: 'utf-8',
parser(text: string): any {
parser(text: string) {
return configListParser(text);
},
}
}

export default function (): Pick<SimpleGit, 'addConfig' | 'listConfig'> {
export default function (): Pick<SimpleGit, 'addConfig' | 'getConfig' | 'listConfig'> {
return {
addConfig(this: SimpleGitApi, key: string, value: string, ...rest: unknown[]) {
return this._runTask(
Expand All @@ -61,6 +77,13 @@ export default function (): Pick<SimpleGit, 'addConfig' | 'listConfig'> {
);
},

getConfig(this: SimpleGitApi, key: string, scope?: GitConfigScope) {
return this._runTask(
getConfigTask(key, asConfigScope(scope, undefined)),
trailingFunctionArgument(arguments),
)
},

listConfig(this: SimpleGitApi, ...rest: unknown[]) {
return this._runTask(
listConfigTask(asConfigScope(rest[0], undefined)),
Expand Down
62 changes: 62 additions & 0 deletions test/unit/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,66 @@ custom@mydev.co\0`);
assertExecutedCommands('config', '--list', '--show-origin', '--null', '--system');
});

describe('getConfig', () => {
it('exposes all values split by scope', async () => {
const task = git.getConfig('some.prop');
await closeWithSuccess('file:.git/blah\0some.prop\nvalue\0file:.git/config\0some.prop\nvalue1\0file:.git/config\0some.prop\nvalue2\0');
const {scopes} = await task;

expect(scopes).toEqual(new Map([
['.git/blah', ['value']],
['.git/config', ['value1', 'value2']],
]))
});

it('ignores properties with mismatched key', async () => {
const task = git.getConfig('some.prop');
await closeWithSuccess('file:.git/blah\0other.prop\nvalue\0file:.git/config\0some.prop\nvalue1\0file:.git/config\0other.prop\nvalue2\0');
const {value, values, scopes} = await task;

expect(value).toBe('value1');
expect(values).toEqual(['value1']);
expect(scopes).toEqual(new Map([
['.git/config', ['value1']],
]))
});

it('gets a single item', async () => {
const task = git.getConfig('foo');
await closeWithSuccess(`file:/Users/me/.gitconfig\0foo
bar\0file:.git/config\0foo
baz\0`);

expect(await task).toEqual(like({
key: 'foo',
value: 'baz',
values: ['bar', 'baz'],
paths: ['/Users/me/.gitconfig', '.git/config'],
}));
assertExecutedCommands('config', '--null', '--show-origin', '--get-all', 'foo');
});

it('gets a single item in a specific scope', async () => {
const task = git.getConfig('user.email', 'local');
await closeWithSuccess(`file:.git/config\0user.email
another@mydev.co\0file:.git/config\0user.email
final@mydev.co\0`);

expect(await task).toEqual(like({
value: 'final@mydev.co',
values: ['another@mydev.co', 'final@mydev.co'],
paths: ['.git/config'],
}));
assertExecutedCommands('config', '--local', '--null', '--show-origin', '--get-all', 'user.email');
});

it('allows callbacks when getting a single item', async () => {
const callback = jest.fn();
git.getConfig('foo', GitConfigScope.system, callback);
await closeWithSuccess(`file:/Users/me/.gitconfig\0foo\nbar\0\n\n`);

expect(callback).toHaveBeenCalledWith(null, like({value: 'bar'}));
});
});

});
22 changes: 22 additions & 0 deletions typings/response.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ export interface CommitResult {
};
}

/** Represents the response to using `git.getConfig` */
export interface ConfigGetResult {
/** The key that was searched for */
key: string;

/** The single value seen by `git` for this key (equivalent to `git config --get key`) */
value: string | null;

/** All possible values for this key no matter the scope (equivalent to `git config --get-all key`) */
values: string[];

/** The file paths from which configuration was read */
paths: string[];

/**
* The full hierarchy of values the property can have had across the
* various scopes that were searched (keys in this Map are the strings
* also found in the `paths` array).
*/
scopes: Map<string, string[]>;
}

/**
* Represents the current git configuration, as defined by the output from `git log`
*/
Expand Down
11 changes: 9 additions & 2 deletions typings/simple-git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export interface SimpleGit extends SimpleGitBase {
* Add config to local git instance for the specified `key` (eg: user.name) and value (eg: 'your name').
* Set `append` to true to append to rather than overwrite the key
*/
addConfig(key: string, value: string, append?: boolean, scope?: 'system' | 'global' | 'local' | 'worktree', callback?: types.SimpleGitTaskCallback<string>): Response<string>;
addConfig(key: string, value: string, append?: boolean, scope?: keyof typeof types.GitConfigScope, callback?: types.SimpleGitTaskCallback<string>): Response<string>;

addConfig(key: string, value: string, append?: boolean, callback?: types.SimpleGitTaskCallback<string>): Response<string>;

Expand All @@ -134,7 +134,7 @@ export interface SimpleGit extends SimpleGitBase {
/**
* Configuration values visible to git in the current working directory
*/
listConfig(scope: types.GitConfigScope | string, callback?: types.SimpleGitTaskCallback<resp.ConfigListSummary>): Response<resp.ConfigListSummary>;
listConfig(scope: keyof typeof types.GitConfigScope, callback?: types.SimpleGitTaskCallback<resp.ConfigListSummary>): Response<resp.ConfigListSummary>;

listConfig(callback?: types.SimpleGitTaskCallback<resp.ConfigListSummary>): Response<resp.ConfigListSummary>;

Expand Down Expand Up @@ -370,6 +370,13 @@ export interface SimpleGit extends SimpleGitBase {

fetch(callback?: types.SimpleGitTaskCallback<resp.FetchResult>): Response<resp.FetchResult>;

/**
* Gets the current value of a configuration property by it key, optionally specify the scope in which
* to run the command (omit / set to `undefined` to check in the complete overlaid configuration visible
* to the `git` process).
*/
getConfig(key: string, scope?: keyof typeof types.GitConfigScope, callback?: types.SimpleGitTaskCallback<string>): Response<resp.ConfigGetResult>;

/**
* Gets the currently available remotes, setting the optional verbose argument to true includes additional
* detail on the remotes themselves.
Expand Down

0 comments on commit 5c9c660

Please sign in to comment.