@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
135 lines (128 loc) • 5.11 kB
text/typescript
/*
* Copyright © 2020 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocalProject } from "@atomist/automation-client/lib/project/local/LocalProject";
import { execPromise } from "@atomist/automation-client/lib/util/child_process";
import { SdmGoalEvent } from "../../../api/goal/SdmGoalEvent";
import { ProgressLog } from "../../../spi/log/ProgressLog";
import { ChangeType } from "./change";
/**
* Glob pattern used to identify Kubernetes spec files in a
* repository. It only matched JSON and YAML files at the root of the
* repository.
*/
export const k8sSpecGlob = "*.@(json|yaml|yml)";
/**
* Regular expression used to identify Kubernetes spec files in a
* repository. It only matched JSON and YAML files at the root of the
* repository.
*/
export const k8sSpecRegExp = /^[^/]+\.(?:json|ya?ml)$/;
/** Container for changes in a commit. */
export interface PushDiff {
/** Whether to apply or delete the change. */
change: ChangeType;
/** Path to spec relative to the project base directory. */
path: string;
/** Git SHA of change. */
sha: string;
}
/**
* Determine all changed Kubernetes resource spec files in a push. A
* file is considered a Kubernetes resource spec if it matches
* [[k8sSpecRegExp]]. Changes are returned in commit order, with the
* commit order unchanged from the push event sent by cortex, which it
* typically chronologically with the oldest commit first. Within a
* commit, the changes are sorted first by operation, with deletes
* before applies, and then by path using `sort(localCompare)`.
* Deletes are sorted first because renames are processed as adds and
* deletes and we want to avoid a rename resulting in a resource being
* deleted. If you want to control the order of operations, spread
* the operations across multiple commits in the same push.
*
* @param project project with the changed Kubernetes resource specs.
* @param push git push with changes
* @param tag git commit message tag indicating automated commits that should be ignored
* @param log goal execution progress log
* @return sorted resource spec changes
*/
export async function diffPush(project: LocalProject, push: SdmGoalEvent["push"], tag: string, log: ProgressLog): Promise<PushDiff[]> {
const changes: PushDiff[] = [];
const commits = push.commits.filter(c => !c.message.includes(tag));
for (const commit of commits) {
try {
const sha = commit.sha;
const args = ["diff", "-z", "--no-renames", "--name-status", "--diff-filter=ADM", `${sha}~1`, sha, "--"];
const opts = { cwd: project.baseDir };
const diffResult = await execPromise("git", args, opts);
const newChanges = parseNameStatusDiff(sha, diffResult.stdout);
changes.push(...newChanges);
} catch (e) {
e.message = `Failed to diff commit ${commit.sha}, skipping: ${e.message}`;
log.write(e.message);
}
}
return changes;
}
/**
* Convert output of `git diff -z --name-status` to a list of file
* changes. The returned changes are sorted by file path.
*
* @param sha git commit SHA
* @param diff output from git diff command
* @return sorted resource spec changes
*/
export function parseNameStatusDiff(sha: string, diff: string): PushDiff[] {
const changes: PushDiff[] = [];
const diffContent = diff.trim().replace(/\0$/, "");
if (!diffContent) {
return changes;
}
const fields = diffContent.split("\0");
for (let i = 0; i < fields.length; i += 2) {
const f = fields[i];
if (!f) {
throw new Error(`Empty git diff status`);
}
const path = fields[i + 1];
if (!path) {
throw new Error(`Missing path from git diff status: ${f}`);
}
if (!k8sSpecRegExp.test(path)) {
continue;
}
if (f === "A" || f.startsWith("M")) {
changes.push({ change: "apply", path, sha });
} else if (f === "D") {
changes.push({ change: "delete", path, sha });
} else {
throw new Error(`Unexpected git diff status for path '${path}' in commit ${sha}: ${f}`);
}
}
return changes.sort(compareDiff);
}
/**
* Sort "delete" changes before "apply". Within each category, sort
* by path.
*/
function compareDiff(a: PushDiff, b: PushDiff): number {
if (a.change === b.change) {
return a.path.localeCompare(b.path);
} else if (a.change === "delete") {
return -1;
} else {
return 1;
}
}