Skip to content

Commit

Permalink
feat: allow excluding file paths to filter out commits (#1875)
Browse files Browse the repository at this point in the history
* feat: add exclude for manifest packages

* chore: adjust ReleaserConfigJson interface

* docs: update documentation

* feat: read exclude-paths from configuration

* chore: add license to new files

* fix: include only based on relevant files

* use array index, Array.prototype.at is not available in node 14 which we still support

---------

Co-authored-by: Jeff Ching <[email protected]>
  • Loading branch information
alex-enchi and chingor13 authored Apr 10, 2023
1 parent fde2602 commit a9bed82
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 25 deletions.
2 changes: 2 additions & 0 deletions docs/manifest-releaser.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ defaults (those are documented in comments)
".": {
// overrides release-type for node
"release-type": "node",
// exclude commits from that path from processing
"exclude-paths": ["path/to/myPyPkgA"]
},

// path segment should be relative to repository root
Expand Down
10 changes: 9 additions & 1 deletion schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@
]
}
},
"exclude-paths": {
"description": "Path of commits to be excluded from parsing. If all files from commit belong to one of the paths it will be skipped",
"type": "array",
"items": {
"type": "string"
}
},
"version-file": {
"description": "Path to the specialize version file. Used by `ruby` and `simple` strategies.",
"type": "string"
Expand Down Expand Up @@ -394,6 +401,7 @@
"extra-files": true,
"version-file": true,
"snapshot-label": true,
"initial-version": true
"initial-version": true,
"exclude-paths": true
}
}
10 changes: 9 additions & 1 deletion src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
FilePullRequestOverflowHandler,
} from './util/pull-request-overflow-handler';
import {signoffCommitMessage} from './util/signoff-commit-message';
import {CommitExclude} from './util/commit-exclude';

type ExtraJsonFile = {
type: 'json';
Expand Down Expand Up @@ -125,6 +126,8 @@ export interface ReleaserConfig {
extraFiles?: ExtraFile[];
snapshotLabels?: string[];
skipSnapshot?: boolean;
// Manifest only
excludePaths?: string[];
}

export interface CandidateReleasePullRequest {
Expand Down Expand Up @@ -167,6 +170,7 @@ interface ReleaserConfigJson {
'snapshot-label'?: string; // Java-only
'skip-snapshot'?: boolean; // Java-only
'initial-version'?: string;
'exclude-paths'?: string[]; // manifest-only
}

export interface ManifestOptions {
Expand Down Expand Up @@ -637,13 +641,15 @@ export class Manifest {
const splitCommits = cs.split(commits);

// limit paths to ones since the last release
const commitsPerPath: Record<string, Commit[]> = {};
let commitsPerPath: Record<string, Commit[]> = {};
for (const path in this.repositoryConfig) {
commitsPerPath[path] = commitsAfterSha(
path === ROOT_PROJECT_PATH ? commits : splitCommits[path],
releaseShasByPath[path]
);
}
const commitExclude = new CommitExclude(this.repositoryConfig);
commitsPerPath = commitExclude.excludeCommits(commitsPerPath);

// backfill latest release tags from manifest
for (const path in this.repositoryConfig) {
Expand Down Expand Up @@ -1282,6 +1288,7 @@ function extractReleaserConfig(
extraLabels: config['extra-label']?.split(','),
skipSnapshot: config['skip-snapshot'],
initialVersion: config['initial-version'],
excludePaths: config['exclude-paths'],
};
}

Expand Down Expand Up @@ -1616,6 +1623,7 @@ function mergeReleaserConfig(
skipSnapshot: pathConfig.skipSnapshot ?? defaultConfig.skipSnapshot,
initialVersion: pathConfig.initialVersion ?? defaultConfig.initialVersion,
extraLabels: pathConfig.extraLabels ?? defaultConfig.extraLabels,
excludePaths: pathConfig.excludePaths ?? defaultConfig.excludePaths,
};
}

Expand Down
63 changes: 63 additions & 0 deletions src/util/commit-exclude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 Google LLC
//
// 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 {Commit} from '../commit';
import {ReleaserConfig, ROOT_PROJECT_PATH} from '../manifest';
import {normalizePaths} from './commit-utils';

export type CommitExcludeConfig = Pick<ReleaserConfig, 'excludePaths'>;

export class CommitExclude {
private excludePaths: Record<string, string[]> = {};

constructor(config: Record<string, CommitExcludeConfig>) {
Object.entries(config).forEach(([path, releaseConfig]) => {
if (releaseConfig.excludePaths) {
this.excludePaths[path] = normalizePaths(releaseConfig.excludePaths);
}
});
}

excludeCommits<T extends Commit>(
commitsPerPath: Record<string, T[]>
): Record<string, T[]> {
const filteredCommitsPerPath: Record<string, T[]> = {};
Object.entries(commitsPerPath).forEach(([path, commits]) => {
if (this.excludePaths[path]) {
commits = commits.filter(commit =>
this.shouldInclude(commit, this.excludePaths[path], path)
);
}
filteredCommitsPerPath[path] = commits;
});
return filteredCommitsPerPath;
}

private shouldInclude(
commit: Commit,
excludePaths: string[],
packagePath: string
): boolean {
return (
!commit.files ||
!commit.files
.filter(file => this.isRelevant(file, packagePath))
.every(file => excludePaths.some(path => this.isRelevant(file, path)))
);
}

private isRelevant(file: string, path: string) {
return path === ROOT_PROJECT_PATH || file.indexOf(`${path}/`) === 0;
}
}
32 changes: 9 additions & 23 deletions src/util/commit-split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import {Commit} from '../commit';
import {ROOT_PROJECT_PATH} from '../manifest';
import {normalizePaths} from './commit-utils';

export interface CommitSplitOptions {
// Include empty git commits: each empty commit is included
Expand Down Expand Up @@ -54,29 +55,14 @@ export class CommitSplit {
opts = opts || {};
this.includeEmpty = !!opts.includeEmpty;
if (opts.packagePaths) {
const paths: string[] = [];
for (let newPath of opts.packagePaths) {
// The special "." path, representing the root of the module, should be
// ignored by commit-split as it is assigned all commits in manifest.ts
if (newPath === ROOT_PROJECT_PATH) {
continue;
}
// normalize so that all paths have leading and trailing slashes for
// non-overlap validation.
// NOTE: GitHub API always returns paths using the `/` separator,
// regardless of what platform the client code is running on
newPath = newPath.replace(/\/$/, '');
newPath = newPath.replace(/^\//, '');
newPath = newPath.replace(/$/, '/');
newPath = newPath.replace(/^/, '/');
// store them with leading and trailing slashes removed.
newPath = newPath.replace(/\/$/, '');
newPath = newPath.replace(/^\//, '');
paths.push(newPath);
}

// sort by longest paths first
this.packagePaths = paths.sort((a, b) => b.length - a.length);
const paths: string[] = normalizePaths(opts.packagePaths);
this.packagePaths = paths
.filter(path => {
// The special "." path, representing the root of the module, should be
// ignored by commit-split as it is assigned all commits in manifest.ts
return path !== ROOT_PROJECT_PATH;
})
.sort((a, b) => b.length - a.length); // sort by longest paths first
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/util/commit-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2023 Google LLC
//
// 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.

export const normalizePaths = (paths: string[]) => {
return paths.map(path => {
// normalize so that all paths have leading and trailing slashes for
// non-overlap validation.
// NOTE: GitHub API always returns paths using the `/` separator,
// regardless of what platform the client code is running on
let newPath = path.replace(/\/$/, '');
newPath = newPath.replace(/^\//, '');
newPath = newPath.replace(/$/, '/');
newPath = newPath.replace(/^/, '/');
// store them with leading and trailing slashes removed.
newPath = newPath.replace(/\/$/, '');
newPath = newPath.replace(/^\//, '');
return newPath;
});
};
15 changes: 15 additions & 0 deletions test/fixtures/manifest/config/exclude-paths.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"release-type": "simple",
"label": "custom: pending",
"release-label": "custom: tagged",
"exclude-paths": ["path-ignore"],
"packages": {
".": {
"component": "root",
"exclude-paths": ["path-root-ignore"]
},
"node-lib": {
"component": "node-lib"
}
}
}
31 changes: 31 additions & 0 deletions test/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,37 @@ describe('Manifest', () => {
'lang: nodejs',
]);
});
it('should read exclude paths from manifest', async () => {
const getFileContentsStub = sandbox.stub(
github,
'getFileContentsOnBranch'
);
getFileContentsStub
.withArgs('release-please-config.json', 'main')
.resolves(
buildGitHubFileContent(
fixturesPath,
'manifest/config/exclude-paths.json'
)
)
.withArgs('.release-please-manifest.json', 'main')
.resolves(
buildGitHubFileContent(
fixturesPath,
'manifest/versions/versions.json'
)
);
const manifest = await Manifest.fromManifest(
github,
github.repository.defaultBranch
);
expect(manifest.repositoryConfig['.'].excludePaths).to.deep.equal([
'path-root-ignore',
]);
expect(manifest.repositoryConfig['node-lib'].excludePaths).to.deep.equal([
'path-ignore',
]);
});
it('should build simple plugins from manifest', async () => {
const getFileContentsStub = sandbox.stub(
github,
Expand Down
Loading

0 comments on commit a9bed82

Please sign in to comment.