Files
renovate/lib/util/git/instrument.ts
Sebastian Poxhofer 16ef1715e9 refactor(linters): add rules for @sindresorhus/is helpers (#45229)
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules

Flag ad-hoc type checks that have @sindresorhus/is equivalents:
- .filter(Boolean) -> .filter(isTruthy)
- typeof x === 'string' -> isString(x)
- x === null || x === undefined -> isNullOrUndefined(x) (and negation)

The typeof x === 'object' shape lives in a separate suggestion-only rule
(renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject()
are not drop-in equivalents: null, arrays and functions behave differently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: fix renovate/prefer-is-helpers violations

Replace ad-hoc type checks with @sindresorhus/is helpers across lib/:
- 13x .filter(Boolean) -> .filter(isTruthy)
- 36x typeof x ==/!== 'string' -> isString(x) / !isString(x)
- 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation)

Promote renovate/prefer-is-helpers to error now that the codebase is clean.
renovate/prefer-is-object stays at warn: its 15 remaining sites guard
parsed YAML/JSON or unknown values where isObject()/isPlainObject() are
not provably equivalent (null/array/function semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: enforce renovate/prefer-is-object at error level

Resolve the 15 remaining warnings and raise the rule from warn to error:

- Migrate 10 sites to isObject() where the guarded value provably cannot
  be a function (parsed YAML/JSON, typed exec command arguments, or
  test-controlled fixtures): helm-values, woodpecker, bun, pre-commit
  managers, exec utils, file package cache, and the github http spec.
  In the bun manager this also fixes a latent crash where a null
  `workspaces` value would throw on the `in` operator.
- Keep 5 sites as reasoned oxlint-disable comments where excluding
  functions is load-bearing: the byte-exact fingerprint canonicalizer
  (2), the circularity walker, the zod result guard, and the generic
  hasKey helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:22:03 +00:00

294 lines
8.7 KiB
TypeScript

import { isString } from '@sindresorhus/is';
import type {
BranchSummary,
CommitResult,
DefaultLogFields,
DiffResult,
LogOptions,
LogResult,
Options,
RemoteWithRefs,
RemoteWithoutRefs,
Response,
SimpleGit,
StatusResult,
TaskOptions,
VersionResult,
} from 'simple-git';
import { instrument } from '../../instrumentation/index.ts';
import type { RenovateSpanOptions } from '../../instrumentation/types.ts';
import {
ATTR_VCS_GIT_OPERATION_TYPE,
ATTR_VCS_GIT_SUBCOMMAND,
} from '../../instrumentation/types.ts';
import type { GitOperationType } from './types.ts';
function isGitOperationType(
subcommand: string,
): subcommand is GitOperationType {
return knownGitOperationTypesBySubcommand.includes(
subcommand as GitOperationType,
);
}
function gitOperationTypeForSubcommand(subcommand: string): GitOperationType {
let operationType: GitOperationType = 'other';
if (!isGitOperationType(subcommand)) {
if (subcommand === 'update-index') {
operationType = 'plumbing';
}
return operationType;
}
return subcommand;
}
/** single-command prefixes that correspond to an operation type */
const knownGitOperationTypesBySubcommand: GitOperationType[] = [
'branch',
'checkout',
'clean',
'clone',
'commit',
'fetch',
'merge',
'pull',
'push',
'reset',
'submodule',
];
/** helper method for instrumentation of Git operations */
export function prepareInstrumentation(
subcommand: string,
options: RenovateSpanOptions = {},
): {
spanName: string;
options: RenovateSpanOptions;
} {
const operationType = gitOperationTypeForSubcommand(subcommand);
options.attributes ??= {};
options.attributes[ATTR_VCS_GIT_OPERATION_TYPE] = operationType;
options.attributes[ATTR_VCS_GIT_SUBCOMMAND] = subcommand;
return {
spanName: `git ${subcommand}`,
options,
};
}
export function instrumentGit(git: SimpleGit): InstrumentedSimpleGit {
return new InstrumentedSimpleGit(git);
}
/**
* A wrapper for SimpleGit, which is instrumented and adds an ATTR_VCS_GIT_SUBCOMMAND and ATTR_VCS_GIT_OPERATION_TYPE attribute to all calls.
*
* We purposefully use a reduced set of methods, with simpler function parameters, to reduce the complexity needed to instrument the methods that we actually need.
*
* @see SimpleGit
*/
export class InstrumentedSimpleGit {
private git: SimpleGit;
constructor(git: SimpleGit) {
this.git = git;
}
version(): Response<VersionResult> {
return this.git.version();
}
raw(args: string[]): Response<string> {
const { spanName, options } = prepareInstrumentation(args[0]);
return instrument(spanName, () => this.git.raw(args), options);
}
async checkout(whatOrOptions: string | TaskOptions): Promise<string> {
const { spanName, options } = prepareInstrumentation('checkout');
return await instrument(
spanName,
async () => {
if (isString(whatOrOptions)) {
return await this.git.checkout(whatOrOptions);
}
return await this.git.checkout(whatOrOptions);
},
options,
);
}
async checkoutBranch(branch: string, startPoint: string): Promise<void> {
const { spanName, options } = prepareInstrumentation('checkout');
await instrument(
spanName,
() => this.git.checkoutBranch(branch, startPoint),
options,
);
}
async branch(args: string[]): Promise<BranchSummary> {
const { spanName, options } = prepareInstrumentation('branch');
return await instrument(spanName, () => this.git.branch(args), options);
}
async branchLocal(): Promise<BranchSummary> {
const { spanName, options } = prepareInstrumentation('branch');
return await instrument(spanName, () => this.git.branchLocal(), options);
}
async add(files: string | string[]): Promise<string> {
const { spanName, options } = prepareInstrumentation('add');
return await instrument(spanName, () => this.git.add(files), options);
}
async addConfig(key: string, value: string): Promise<void> {
const { spanName, options } = prepareInstrumentation('config');
await instrument(spanName, () => this.git.addConfig(key, value), options);
}
async rm(files: string | string[]): Promise<void> {
const { spanName, options } = prepareInstrumentation('rm');
await instrument(spanName, () => this.git.rm(files), options);
}
async reset(modeOrOptions: any): Promise<void> {
const { spanName, options } = prepareInstrumentation('reset');
await instrument(spanName, () => this.git.reset(modeOrOptions), options);
}
async merge(args: string[]): Promise<void> {
const { spanName, options } = prepareInstrumentation('merge');
await instrument(spanName, () => this.git.merge(args), options);
}
async push(...args: any[]): Promise<any> {
const { spanName, options } = prepareInstrumentation('push');
return await instrument(spanName, () => this.git.push(...args), options);
}
async pull(args: string[]): Promise<any> {
const { spanName, options } = prepareInstrumentation('pull');
return await instrument(spanName, () => this.git.pull(args), options);
}
async fetch(args: string[]): Promise<any> {
const { spanName, options } = prepareInstrumentation('fetch');
return await instrument(spanName, () => this.git.fetch(args), options);
}
async clone(repo: string, dir: string, options: string[]): Promise<void> {
const { spanName, options: spanOptions } = prepareInstrumentation('clone');
await instrument(
spanName,
() => this.git.clone(repo, dir, options),
spanOptions,
);
}
commit(
message: string | string[],
files?: string[],
options?: Options,
): Promise<CommitResult> {
const { spanName, options: spanOptions } = prepareInstrumentation('clone');
return instrument(
spanName,
() => this.git.commit(message, files, options),
spanOptions,
);
}
status(args?: string[]): Response<StatusResult> {
const { spanName, options } = prepareInstrumentation('status');
return instrument(spanName, () => this.git.status(args), options);
}
async log<T = DefaultLogFields>(
options?: TaskOptions | LogOptions<T>,
): Promise<LogResult<T>> {
const { spanName, options: spanOptions } = prepareInstrumentation('log');
return await instrument(spanName, () => this.git.log(options), spanOptions);
}
async revparse(optionOrOptions: string | TaskOptions): Promise<string> {
const { spanName, options } = prepareInstrumentation('log');
return await instrument(
spanName,
async () => {
if (isString(optionOrOptions)) {
return await this.git.revparse(optionOrOptions);
}
return await this.git.revparse(optionOrOptions);
},
options,
);
}
async show(args: string[]): Promise<string> {
const { spanName, options } = prepareInstrumentation('show');
return await instrument(spanName, () => this.git.show(args), options);
}
async diff(args: string[]): Promise<string> {
const { spanName, options } = prepareInstrumentation('diff');
return await instrument(spanName, () => this.git.diff(args), options);
}
async diffSummary(options: TaskOptions): Promise<DiffResult> {
const { spanName, options: spanOptions } = prepareInstrumentation('diff');
return await instrument(
spanName,
() => this.git.diffSummary(options),
spanOptions,
);
}
async getRemotes(
verbose?: boolean,
): Promise<RemoteWithRefs[] | RemoteWithoutRefs[]> {
const { spanName, options } = prepareInstrumentation('remote');
return await instrument(
spanName,
async () => {
if (verbose === true) {
return await instrument('other', () => this.git.getRemotes(true));
}
return await instrument('other', () => this.git.getRemotes());
},
options,
);
}
async addRemote(name: string, repo: string): Promise<void> {
const { spanName, options } = prepareInstrumentation('remote');
await instrument(spanName, () => this.git.addRemote(name, repo), options);
}
env(env: Record<string, string>): this {
this.git = this.git.env(env);
return this;
}
async catFile(args: string[]): Promise<string> {
const { spanName, options } = prepareInstrumentation('remote');
return await instrument(spanName, () => this.git.catFile(args), options);
}
async listRemote(args: string[]): Promise<string> {
const { spanName, options } = prepareInstrumentation('ls-remote');
return await instrument(spanName, () => this.git.listRemote(args), options);
}
async submoduleUpdate(args: string[]): Promise<void> {
const { spanName, options } = prepareInstrumentation('submodule');
await instrument(spanName, () => this.git.submoduleUpdate(args), options);
}
}