@apexdevtools/git-ops 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # test-runner - Changelog
2
+
3
+ ## 1.0.0 - 2023-02-20
4
+
5
+ - Initial release.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c), FinancialForce.com, inc
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ - Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ - Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ - Neither the name of the FinancialForce.com, inc nor the names of its contributors
13
+ may be used to endorse or promote products derived from this software without
14
+ specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
19
+ THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # git-ops
2
+
3
+ Library to do git operations to find changed files in a given git repository.
4
+
5
+ ## Getting Started
6
+
7
+ To build run
8
+
9
+ ```bash
10
+ pnpm install
11
+ pnpm build
12
+ ```
13
+
14
+ Jest unit tests can be run with
15
+
16
+ ```bash
17
+ pnpm test
18
+ ```
19
+
20
+ ##  Usage
21
+
22
+ **Prerequisite**: The head mus tbe set on the repo otherwise any functions using default branch will fail
23
+
24
+ ### Finding changed files
25
+
26
+ Getting changed files using the default branch in that repo given a ref. This find the default branch ion the repo using `git symbolic-ref 'refs/remotes/origin/HEAD'` so the `HEAD` must be set.
27
+ The output of the command is same as running `git diff branchName...ref` combined with `git status`.
28
+
29
+ **Note:** files with the status of deleted (`D`) and ignored (`!`) will not be included in the change set.
30
+
31
+ ```TypeScript
32
+ getDefaultBranchDiffByRef(repoRootDir: string, ref: string): Promise<Set<string>>
33
+ ```
34
+
35
+ Getting changed files using the default branch in that repo.
36
+
37
+ This command is same as calling `getDefaultBranchDiffByRef(repoRootDir, 'HEAD')`.
38
+
39
+ ```TypeScript
40
+ getDefaultBranchDiff(repoRootDir: string): Promise<Set<string>>
41
+ ```
@@ -0,0 +1,2 @@
1
+ export declare function getDefaultBranchDiff(dir: string): Promise<Set<string>>;
2
+ export declare function getDefaultBranchDiffByRef(dir: string, ref: string): Promise<Set<string>>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.getDefaultBranchDiffByRef = exports.getDefaultBranchDiff = void 0;
16
+ const Git_1 = require("../Git/Git");
17
+ function getDefaultBranchDiff(dir) {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ return getDefaultBranchDiffByRef(dir, 'HEAD');
20
+ });
21
+ }
22
+ exports.getDefaultBranchDiff = getDefaultBranchDiff;
23
+ function getDefaultBranchDiffByRef(dir, ref) {
24
+ return __awaiter(this, void 0, void 0, function* () {
25
+ const git = new Git_1.Git(dir);
26
+ return git
27
+ .getDefaultBranchName()
28
+ .then((branchName) => __awaiter(this, void 0, void 0, function* () {
29
+ return yield getChanges(git, branchName, ref);
30
+ }))
31
+ .catch(er => {
32
+ if (er instanceof Error)
33
+ throw Error(`Failed getting diff: ${er.message}`);
34
+ else
35
+ throw Error('Failed getting diff');
36
+ });
37
+ });
38
+ }
39
+ exports.getDefaultBranchDiffByRef = getDefaultBranchDiffByRef;
40
+ function getChanges(git, branchName, ref) {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ const changes = yield Promise.all([
43
+ git.diffRange(branchName, ref),
44
+ git.getLocalChangedAndCreated(),
45
+ ]);
46
+ const allChanges = new Set();
47
+ changes.forEach(set => set.forEach(file => allChanges.add(file)));
48
+ return allChanges;
49
+ });
50
+ }
@@ -0,0 +1,27 @@
1
+ import { SimpleGit } from 'simple-git';
2
+ import { IGit } from '../api/IGit';
3
+ export declare enum FileStatus {
4
+ Unmodified = " ",
5
+ Modified = "M",
6
+ TypeChanged = "T",
7
+ Added = "A",
8
+ Deleted = "D",
9
+ Renamed = "R",
10
+ Copied = "C",
11
+ Updated = "U",
12
+ Untracked = "?",
13
+ Ignore = "!"
14
+ }
15
+ export declare class Git implements IGit {
16
+ private static MIN_GIT_VERSION_MAJOR;
17
+ private static MIN_GIT_VERSION_MINOR;
18
+ private static MIN_GIT_VERSION_PATCH;
19
+ private static gitInstance;
20
+ private dir;
21
+ constructor(dir: string);
22
+ static versionCheck(git: SimpleGit): Promise<void>;
23
+ private get git();
24
+ getDefaultBranchName(): Promise<string>;
25
+ diffRange(fromRef: string, toRef: string): Promise<Set<string>>;
26
+ getLocalChangedAndCreated(): Promise<Set<string>>;
27
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2022, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Git = exports.FileStatus = void 0;
16
+ const simple_git_1 = require("simple-git");
17
+ var FileStatus;
18
+ (function (FileStatus) {
19
+ FileStatus["Unmodified"] = " ";
20
+ FileStatus["Modified"] = "M";
21
+ FileStatus["TypeChanged"] = "T";
22
+ FileStatus["Added"] = "A";
23
+ FileStatus["Deleted"] = "D";
24
+ FileStatus["Renamed"] = "R";
25
+ FileStatus["Copied"] = "C";
26
+ FileStatus["Updated"] = "U";
27
+ FileStatus["Untracked"] = "?";
28
+ FileStatus["Ignore"] = "!";
29
+ })(FileStatus = exports.FileStatus || (exports.FileStatus = {}));
30
+ class Git {
31
+ constructor(dir) {
32
+ this.dir = dir;
33
+ }
34
+ static versionCheck(git) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ const version = yield git.version();
37
+ const isSupported = version.major >= this.MIN_GIT_VERSION_MAJOR &&
38
+ version.minor >= this.MIN_GIT_VERSION_MINOR &&
39
+ version.patch >= this.MIN_GIT_VERSION_PATCH;
40
+ if (!isSupported)
41
+ throw new Error(`Unsupported version of git. Min version must be ${this.MIN_GIT_VERSION_MAJOR}.${this.MIN_GIT_VERSION_MINOR}.${this.MIN_GIT_VERSION_PATCH}`);
42
+ });
43
+ }
44
+ get git() {
45
+ if (Git.gitInstance)
46
+ return Promise.resolve(Git.gitInstance);
47
+ //eslint-disable-next-line
48
+ const git = (0, simple_git_1.simpleGit)(this.dir);
49
+ return Git.versionCheck(git).then(() => {
50
+ Git.gitInstance = git;
51
+ return git;
52
+ });
53
+ }
54
+ getDefaultBranchName() {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ return this.git
57
+ .then(git => git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD', '--short']))
58
+ .catch(error => {
59
+ if (error instanceof Error)
60
+ throw Error(`Failed to find symbolic ref no remote HEAD with message: '${error.message.trim()}'`);
61
+ else
62
+ throw new Error('Failed to find symbolic ref no remote HEAD');
63
+ })
64
+ .then(branch => {
65
+ if (branch.startsWith('origin/'))
66
+ return branch.trim();
67
+ else
68
+ throw new Error(`Expected default branch '${branch}' to start with 'origin/'`);
69
+ });
70
+ });
71
+ }
72
+ diffRange(fromRef, toRef) {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ return this.git
75
+ .then(git => git.diff([`${fromRef}...${toRef}`, '--name-only', '-z']))
76
+ .then(diff => {
77
+ const files = diff
78
+ .split('\u0000')
79
+ .map(s => s.trim())
80
+ .filter(s => s); //Removes any falsy values
81
+ return new Set([...files]);
82
+ });
83
+ });
84
+ }
85
+ getLocalChangedAndCreated() {
86
+ return __awaiter(this, void 0, void 0, function* () {
87
+ const excludeStatus = [FileStatus.Deleted, FileStatus.Ignore];
88
+ return this.git
89
+ .then(git => git.status())
90
+ .then(status => {
91
+ const changedFiles = status.files
92
+ .filter(f => !excludeStatus.includes(f.index) &&
93
+ !excludeStatus.includes(f.working_dir))
94
+ .map(f => f.path);
95
+ return new Set(...[changedFiles]);
96
+ });
97
+ });
98
+ }
99
+ }
100
+ exports.Git = Git;
101
+ Git.MIN_GIT_VERSION_MAJOR = 2;
102
+ Git.MIN_GIT_VERSION_MINOR = 37;
103
+ Git.MIN_GIT_VERSION_PATCH = 0;
104
+ Git.gitInstance = undefined;
@@ -0,0 +1,5 @@
1
+ export interface IGit {
2
+ getDefaultBranchName(): Promise<string>;
3
+ getLocalChangedAndCreated(): Promise<Set<string>>;
4
+ diffRange(fromRef: string, toRef: string): Promise<Set<string>>;
5
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export { getDefaultBranchDiff, getDefaultBranchDiffByRef, } from './FilesChanged/BranchChanges';
2
+ export { IGit } from './api/IGit';
3
+ export { Git } from './Git/Git';
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Git = exports.getDefaultBranchDiffByRef = exports.getDefaultBranchDiff = void 0;
7
+ var BranchChanges_1 = require("./FilesChanged/BranchChanges");
8
+ Object.defineProperty(exports, "getDefaultBranchDiff", { enumerable: true, get: function () { return BranchChanges_1.getDefaultBranchDiff; } });
9
+ Object.defineProperty(exports, "getDefaultBranchDiffByRef", { enumerable: true, get: function () { return BranchChanges_1.getDefaultBranchDiffByRef; } });
10
+ var Git_1 = require("./Git/Git");
11
+ Object.defineProperty(exports, "Git", { enumerable: true, get: function () { return Git_1.Git; } });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const Git_1 = require("../../src/Git/Git");
16
+ const BranchChanges_1 = require("../../src/FilesChanged/BranchChanges");
17
+ const mockGitImpl = {
18
+ getDefaultBranchName: jest.fn(),
19
+ getLocalChangedAndCreated: jest.fn(),
20
+ diffRange: jest.fn(),
21
+ };
22
+ jest.mock('../../src/Git/Git', () => {
23
+ return {
24
+ Git: jest.fn().mockImplementation(() => mockGitImpl),
25
+ };
26
+ });
27
+ const mockedGit = jest.mocked(Git_1.Git, { shallow: true });
28
+ describe('Branch changes', () => {
29
+ afterEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+ describe('getDefaultBranchDiffByRef', () => {
33
+ it('returns the correct set of files when git ops resolve', () => __awaiter(void 0, void 0, void 0, function* () {
34
+ //Given
35
+ mockGitImpl.getDefaultBranchName.mockResolvedValue('default/branch/name');
36
+ mockGitImpl.diffRange.mockResolvedValue(new Set(['File.txt', 'AnotherClass.txt']));
37
+ mockGitImpl.getLocalChangedAndCreated.mockResolvedValue(new Set(['SomeFile.txt']));
38
+ //When
39
+ const res = yield (0, BranchChanges_1.getDefaultBranchDiffByRef)('./some/path/to/dir', 'abc12fcd');
40
+ //Then
41
+ expect(mockedGit).toHaveBeenCalledWith('./some/path/to/dir');
42
+ expect(mockGitImpl.getDefaultBranchName).toBeCalledTimes(1);
43
+ expect(mockGitImpl.getLocalChangedAndCreated).toBeCalledTimes(1);
44
+ expect(mockGitImpl.diffRange).toHaveBeenCalledWith('default/branch/name', 'abc12fcd');
45
+ expect(res).toEqual(new Set(['File.txt', 'AnotherClass.txt', 'SomeFile.txt']));
46
+ }));
47
+ it('rejects and throws error when "getDefaultBranchName" fails', () => __awaiter(void 0, void 0, void 0, function* () {
48
+ //Given
49
+ mockGitImpl.getDefaultBranchName.mockRejectedValue(Error('no head ref error'));
50
+ //When/Then
51
+ yield expect((0, BranchChanges_1.getDefaultBranchDiffByRef)('./some/path/to/dir', 'abc12fcd')).rejects.toEqual(Error('Failed getting diff: no head ref error'));
52
+ }));
53
+ it('rejects and throws error when "getLocalChangedAndCreated" fails', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ //Given
55
+ mockGitImpl.getDefaultBranchName.mockResolvedValue('default/branch/name');
56
+ mockGitImpl.diffRange.mockResolvedValue(new Set(['File.txt', 'AnotherClass.txt']));
57
+ mockGitImpl.getLocalChangedAndCreated.mockRejectedValue(Error('op failed'));
58
+ //When/Then
59
+ yield expect((0, BranchChanges_1.getDefaultBranchDiffByRef)('./some/path/to/dir', 'abc12fcd')).rejects.toEqual(Error('Failed getting diff: op failed'));
60
+ }));
61
+ });
62
+ describe('getDefaultBranchDiff', () => {
63
+ it('returns the correct set of files', () => __awaiter(void 0, void 0, void 0, function* () {
64
+ //Given
65
+ mockGitImpl.getDefaultBranchName.mockResolvedValue('default/branch/name');
66
+ mockGitImpl.diffRange.mockResolvedValue(new Set(['File.txt', 'AnotherClass.txt']));
67
+ mockGitImpl.getLocalChangedAndCreated.mockResolvedValue(new Set(['SomeFile.txt']));
68
+ //When
69
+ const res = yield (0, BranchChanges_1.getDefaultBranchDiff)('./some/path/to/dir');
70
+ //Then
71
+ expect(mockedGit).toHaveBeenCalledWith('./some/path/to/dir');
72
+ expect(mockGitImpl.getDefaultBranchName).toBeCalledTimes(1);
73
+ expect(mockGitImpl.getLocalChangedAndCreated).toBeCalledTimes(1);
74
+ expect(mockGitImpl.diffRange).toHaveBeenCalledWith('default/branch/name', 'HEAD');
75
+ expect(res).toEqual(new Set(['File.txt', 'AnotherClass.txt', 'SomeFile.txt']));
76
+ }));
77
+ it('rejects and throws error when "getLocalChangedAndCreated" fails', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ //Given
79
+ mockGitImpl.getDefaultBranchName.mockResolvedValue('default/branch/name');
80
+ mockGitImpl.diffRange.mockResolvedValue(new Set(['File.txt', 'AnotherClass.txt']));
81
+ mockGitImpl.getLocalChangedAndCreated.mockRejectedValue(Error('op failed'));
82
+ //When/Then
83
+ yield expect((0, BranchChanges_1.getDefaultBranchDiff)('./some/path/to/dir')).rejects.toEqual(Error('Failed getting diff: op failed'));
84
+ }));
85
+ });
86
+ });
@@ -0,0 +1,24 @@
1
+ export declare class RepoManager {
2
+ private isInit;
3
+ private rootDir;
4
+ private remoteRepoDir;
5
+ private testRepoDir;
6
+ private static instances;
7
+ private constructor();
8
+ static getInstance(rootDir: string): RepoManager;
9
+ private get git();
10
+ private initRemoteRepo;
11
+ private cloneRepo;
12
+ get repoDir(): string;
13
+ init(): Promise<void>;
14
+ setHead(): Promise<string | void>;
15
+ push(): Promise<void | import("simple-git").PushResult>;
16
+ checkout(branchName: string, from: string): Promise<void>;
17
+ stageAll(files?: string[]): Promise<string>;
18
+ getGitLog(): Promise<readonly (import("simple-git").DefaultLogFields & import("simple-git").ListLogLine)[]>;
19
+ stageAndCommitAll(files?: string[]): Promise<import("simple-git").CommitResult>;
20
+ tearDown(): Promise<void>;
21
+ createOrUpdateFile(fileName: string, content: string): Promise<void>;
22
+ rmFile(file: string): void;
23
+ renameFileInRepo(from: string, to: string): void;
24
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.RepoManager = void 0;
19
+ const simple_git_1 = __importDefault(require("simple-git"));
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ class RepoManager {
23
+ constructor(rootDir) {
24
+ this.isInit = false;
25
+ this.rootDir = rootDir;
26
+ this.remoteRepoDir = rootDir + '/remote.git';
27
+ this.testRepoDir = rootDir + '/test-repo';
28
+ }
29
+ static getInstance(rootDir) {
30
+ if (!this.instances.has(rootDir)) {
31
+ this.instances.set(rootDir, new RepoManager(rootDir));
32
+ }
33
+ return this.instances.get(rootDir);
34
+ }
35
+ get git() {
36
+ if (!this.isInit) {
37
+ throw new Error('Cannot perform git operation before calling init()');
38
+ }
39
+ // eslint-disable-next-line
40
+ return (0, simple_git_1.default)(this.testRepoDir);
41
+ }
42
+ initRemoteRepo() {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ if (!fs_1.default.existsSync(this.remoteRepoDir)) {
45
+ fs_1.default.mkdirSync(this.remoteRepoDir, { recursive: true });
46
+ // eslint-disable-next-line
47
+ const git = (0, simple_git_1.default)(this.remoteRepoDir);
48
+ yield git.init(true);
49
+ }
50
+ });
51
+ }
52
+ cloneRepo() {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ if (fs_1.default.existsSync(this.remoteRepoDir) && !fs_1.default.existsSync(this.testRepoDir)) {
55
+ // eslint-disable-next-line
56
+ yield (0, simple_git_1.default)().clone(this.remoteRepoDir, this.testRepoDir);
57
+ }
58
+ });
59
+ }
60
+ get repoDir() {
61
+ return this.testRepoDir;
62
+ }
63
+ init() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ yield this.initRemoteRepo();
66
+ yield this.cloneRepo();
67
+ this.isInit = true;
68
+ yield this.git.raw(['checkout', '-b', 'main']);
69
+ });
70
+ }
71
+ setHead() {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ return yield this.git
74
+ .raw(['symbolic-ref', 'HEAD', 'refs/heads/main'])
75
+ .then(() => this.git.remote(['set-head', 'origin', 'main']))
76
+ .catch(err => console.log('set head failed ', err));
77
+ });
78
+ }
79
+ push() {
80
+ return __awaiter(this, void 0, void 0, function* () {
81
+ return yield this.git
82
+ .push('origin', 'main', ['-u'])
83
+ .catch(err => console.log('push failed ', err));
84
+ });
85
+ }
86
+ checkout(branchName, from) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ return this.git.checkoutBranch(branchName, from);
89
+ });
90
+ }
91
+ stageAll(files = ['.']) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ return this.git.add(files);
94
+ });
95
+ }
96
+ getGitLog() {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ const log = yield this.git.log();
99
+ return log.all;
100
+ });
101
+ }
102
+ stageAndCommitAll(files) {
103
+ return __awaiter(this, void 0, void 0, function* () {
104
+ return this.stageAll(files).then(() => this.git.commit('commit message'));
105
+ });
106
+ }
107
+ tearDown() {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ yield fs_1.default.promises.rm(this.rootDir, { recursive: true, force: true });
110
+ });
111
+ }
112
+ createOrUpdateFile(fileName, content) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ const fPath = path_1.default.join(this.testRepoDir, fileName);
115
+ if (fs_1.default.existsSync(fPath)) {
116
+ yield fs_1.default.promises.appendFile(fPath, content);
117
+ }
118
+ else {
119
+ yield fs_1.default.promises.writeFile(fPath, content);
120
+ }
121
+ });
122
+ }
123
+ rmFile(file) {
124
+ fs_1.default.unlinkSync(path_1.default.join(this.testRepoDir, file));
125
+ }
126
+ renameFileInRepo(from, to) {
127
+ const fromPath = path_1.default.join(this.testRepoDir, from);
128
+ const toPath = path_1.default.join(this.testRepoDir, to);
129
+ fs_1.default.renameSync(fromPath, toPath);
130
+ }
131
+ }
132
+ exports.RepoManager = RepoManager;
133
+ RepoManager.instances = new Map();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2023, FinancialForce.com, inc. All rights reserved.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const Git_1 = require("../../src/Git/Git");
16
+ const RepoManager_1 = require("../FsUtils/RepoManager");
17
+ describe('Git', () => {
18
+ const repoManager = RepoManager_1.RepoManager.getInstance('./test/repos');
19
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
20
+ yield repoManager.init();
21
+ }));
22
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
23
+ yield repoManager.tearDown();
24
+ }));
25
+ describe('version check', () => {
26
+ it('does not fail when Git version is equal to 2.37.0', () => {
27
+ //Given/When
28
+ const mock = {
29
+ version: jest.fn().mockReturnValue({ major: 2, minor: 37, patch: 0 }),
30
+ };
31
+ expect(Git_1.Git.versionCheck(mock)).resolves;
32
+ });
33
+ it('fails when Git major version is lower', () => __awaiter(void 0, void 0, void 0, function* () {
34
+ //Given/When
35
+ const mock = {
36
+ version: jest.fn().mockReturnValue({ major: 1 }),
37
+ };
38
+ //Then
39
+ yield expect(Git_1.Git.versionCheck(mock)).rejects.toThrow(Error('Unsupported version of git. Min version must be 2.37.0'));
40
+ }));
41
+ it('fails when Git minor version is lower', () => __awaiter(void 0, void 0, void 0, function* () {
42
+ //Given/When
43
+ const mock = {
44
+ version: jest.fn().mockReturnValue({ major: 3, minor: 36 }),
45
+ };
46
+ //Then
47
+ yield expect(Git_1.Git.versionCheck(mock)).rejects.toThrow(Error('Unsupported version of git. Min version must be 2.37.0'));
48
+ }));
49
+ it('fails when Git patch version is more than 0', () => {
50
+ //Given/When
51
+ const mock = {
52
+ version: jest.fn().mockReturnValue({ major: 3, minor: 38, patch: 1 }),
53
+ };
54
+ //Then
55
+ expect(Git_1.Git.versionCheck(mock)).resolves;
56
+ });
57
+ });
58
+ describe('branch operation', () => {
59
+ it('fails to find default branch name when head is not set', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ //Given/When/Then
61
+ yield expect(new Git_1.Git(repoManager.repoDir).getDefaultBranchName()).rejects.toThrow(Error("Failed to find symbolic ref no remote HEAD with message: 'fatal: ref refs/remotes/origin/HEAD is not a symbolic ref'"));
62
+ }));
63
+ it('find default branch name when head is set', () => __awaiter(void 0, void 0, void 0, function* () {
64
+ //Given
65
+ yield repoManager
66
+ .createOrUpdateFile('file.txt', 'Test Text')
67
+ .then(() => repoManager.stageAndCommitAll(['file.txt']))
68
+ .then(() => repoManager.push())
69
+ .then(() => repoManager.setHead());
70
+ //When
71
+ const branchName = yield new Git_1.Git(repoManager.repoDir).getDefaultBranchName();
72
+ //Then
73
+ expect(branchName).toBe('origin/main');
74
+ }));
75
+ it('find default branch name when head is set', () => __awaiter(void 0, void 0, void 0, function* () {
76
+ //Given
77
+ yield repoManager
78
+ .createOrUpdateFile('file.txt', 'Test Text')
79
+ .then(() => repoManager.stageAndCommitAll(['file.txt']))
80
+ .then(() => repoManager.push())
81
+ .then(() => repoManager.setHead());
82
+ //When
83
+ const branchName = yield new Git_1.Git(repoManager.repoDir).getDefaultBranchName();
84
+ //Then
85
+ expect(branchName).toBe('origin/main');
86
+ }));
87
+ });
88
+ describe('file change operations', () => {
89
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
90
+ yield repoManager
91
+ .createOrUpdateFile('file.txt', 'text')
92
+ .then(() => repoManager.stageAndCommitAll(['file.txt']))
93
+ .then(() => repoManager.push())
94
+ .then(() => repoManager.setHead());
95
+ }));
96
+ describe('local changes', () => {
97
+ it('finds modified files', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ //Given
99
+ yield repoManager.createOrUpdateFile('file.txt', 'modify');
100
+ //When
101
+ const files = yield new Git_1.Git(repoManager.repoDir).getLocalChangedAndCreated();
102
+ //Then
103
+ expect(files).toEqual(new Set(['file.txt']));
104
+ }));
105
+ it('finds unstaged renamed files', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ //Given
107
+ repoManager.renameFileInRepo('file.txt', 'renamed.txt');
108
+ // await repoManager.gitStage();
109
+ //When
110
+ const files = yield new Git_1.Git(repoManager.repoDir).getLocalChangedAndCreated();
111
+ //Then
112
+ expect(files).toEqual(new Set(['renamed.txt']));
113
+ }));
114
+ it('finds unstaged new files', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ //Given
116
+ yield repoManager.createOrUpdateFile('newFile.txt', 'content');
117
+ //When
118
+ const files = yield new Git_1.Git(repoManager.repoDir).getLocalChangedAndCreated();
119
+ //Then
120
+ expect(files).toEqual(new Set(['newFile.txt']));
121
+ }));
122
+ it('does not include deleted files', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ //Given
124
+ repoManager.rmFile('file.txt');
125
+ //When
126
+ const files = yield new Git_1.Git(repoManager.repoDir).getLocalChangedAndCreated();
127
+ //Then
128
+ expect(files).toEqual(new Set());
129
+ }));
130
+ it('finds staged renamed, modified and new files ', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ //Setup for renamed file
132
+ yield repoManager
133
+ .createOrUpdateFile('second.txt', 'txt')
134
+ .then(() => repoManager.stageAndCommitAll(['second.txt']));
135
+ //Given
136
+ yield repoManager
137
+ .createOrUpdateFile('file.txt', 'modifiy')
138
+ .then(() => repoManager.renameFileInRepo('second.txt', 'renamed.txt'))
139
+ .then(() => repoManager.createOrUpdateFile('newFile.txt', 'content'))
140
+ .then(() => repoManager.stageAll());
141
+ //When
142
+ const files = yield new Git_1.Git(repoManager.repoDir).getLocalChangedAndCreated();
143
+ //Then
144
+ expect(files).toEqual(new Set(['file.txt', 'renamed.txt', 'newFile.txt']));
145
+ }));
146
+ });
147
+ describe('branch diff changes', () => {
148
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
149
+ yield repoManager
150
+ .checkout('dev', 'main')
151
+ .then(() => repoManager.createOrUpdateFile('newFile.txt', 'text'))
152
+ .then(() => repoManager.stageAndCommitAll(['newFile.txt']));
153
+ }));
154
+ it('finds diff against for default branch and HEAD', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ //Given
156
+ yield repoManager
157
+ .createOrUpdateFile('anotherFileForCommit.txt', 'text')
158
+ .then(() => repoManager.stageAndCommitAll(['anotherFileForCommit.txt']));
159
+ //When
160
+ const files = yield new Git_1.Git(repoManager.repoDir).diffRange('origin/main', 'HEAD');
161
+ //Then
162
+ expect(files).toEqual(new Set(['newFile.txt', 'anotherFileForCommit.txt']));
163
+ }));
164
+ it('finds diff against default branch and commit ref', () => __awaiter(void 0, void 0, void 0, function* () {
165
+ //Given
166
+ const currentRef = (yield repoManager.getGitLog())[0].hash;
167
+ yield repoManager
168
+ .createOrUpdateFile('anotherFileForCommit.txt', 'text')
169
+ .then(() => repoManager.stageAndCommitAll(['anotherFileForCommit.txt']));
170
+ //When
171
+ const files = yield new Git_1.Git(repoManager.repoDir).diffRange('origin/main', currentRef);
172
+ //Then
173
+ expect(files).toEqual(new Set(['newFile.txt']));
174
+ }));
175
+ });
176
+ });
177
+ });
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "@apexdevtools/git-ops",
3
+ "version": "1.0.0",
4
+ "description": "Library to do git operations to find changed files in a given git repository",
5
+ "author": {
6
+ "name": "Apex Dev Tools Team",
7
+ "email": "apexdevtools@gmail.com",
8
+ "url": "https://github.com/apex-dev-tools"
9
+ },
10
+ "main": "./lib/src/index.js",
11
+ "license": "BSD-3-Clause",
12
+ "files": [
13
+ "lib/**/*",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/apex-dev-tools/git-ops.git"
19
+ },
20
+ "engines": {
21
+ "node": ">=14.0"
22
+ },
23
+ "keywords": [
24
+ "salesforce",
25
+ "apex",
26
+ "apexlink"
27
+ ],
28
+ "bugs": {
29
+ "url": "https://github.com/apex-dev-tools/git-ops/issues"
30
+ },
31
+ "homepage": "https://github.com/apex-dev-tools/git-ops#readme",
32
+ "devDependencies": {
33
+ "@ryansonshine/commitizen": "4.2.8",
34
+ "@ryansonshine/cz-conventional-changelog": "3.3.4",
35
+ "@types/jest": "^29.4.0",
36
+ "@types/node": "^18.11.19",
37
+ "@typescript-eslint/eslint-plugin": "^5.52.0",
38
+ "@typescript-eslint/parser": "^5.0.0",
39
+ "conventional-changelog-conventionalcommits": "5.0.0",
40
+ "eslint": "7.25.0",
41
+ "eslint-config-prettier": "8.3.0",
42
+ "eslint-plugin-node": "11.1.0",
43
+ "eslint-plugin-prettier": "3.4.0",
44
+ "husky": "^8.0.0",
45
+ "jest": "^29.4.2",
46
+ "lint-staged": "10.5.4",
47
+ "prettier": "2.2.1",
48
+ "semantic-release": "19.0.2",
49
+ "ts-jest": "^29.0.5",
50
+ "typescript": "^4.9.5"
51
+ },
52
+ "dependencies": {
53
+ "install": "^0.13.0",
54
+ "simple-git": "^3.16.0"
55
+ },
56
+ "config": {
57
+ "commitizen": {
58
+ "path": "./node_modules/@ryansonshine/cz-conventional-changelog"
59
+ }
60
+ },
61
+ "lint-staged": {
62
+ "*.ts": "eslint --cache --cache-location .eslintcache --fix"
63
+ },
64
+ "release": {
65
+ "branches": [
66
+ "main"
67
+ ],
68
+ "plugins": [
69
+ [
70
+ "@semantic-release/commit-analyzer",
71
+ {
72
+ "preset": "conventionalcommits",
73
+ "releaseRules": [
74
+ {
75
+ "type": "build",
76
+ "scope": "deps",
77
+ "release": "patch"
78
+ }
79
+ ]
80
+ }
81
+ ],
82
+ [
83
+ "@semantic-release/release-notes-generator",
84
+ {
85
+ "preset": "conventionalcommits",
86
+ "presetConfig": {
87
+ "types": [
88
+ {
89
+ "type": "feat",
90
+ "section": "Features"
91
+ },
92
+ {
93
+ "type": "fix",
94
+ "section": "Bug Fixes"
95
+ },
96
+ {
97
+ "type": "build",
98
+ "section": "Dependencies and Other Build Updates",
99
+ "hidden": false
100
+ }
101
+ ]
102
+ }
103
+ }
104
+ ],
105
+ "@semantic-release/npm",
106
+ "@semantic-release/github"
107
+ ]
108
+ },
109
+ "scripts": {
110
+ "build": "tsc",
111
+ "clean": "rm -rf ./lib/",
112
+ "cm": "cz",
113
+ "lint": "eslint ./src/ --fix",
114
+ "semantic-release": "semantic-release",
115
+ "test:watch": "jest --watch",
116
+ "test": "jest --coverage --runInBand",
117
+ "typecheck": "tsc --noEmit"
118
+ }
119
+ }