mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 18:41:14 +00:00
feat: Environments release using source control (#6653)
* initial telemetry setup and adjusted pull return * quicksave before merge * feat: add conflicting workflow list to pull modal * feat: update source control pull modal * fix: fix linting issue * feat: add Enter keydown event for submitting source control push modal (no-changelog) feat: add Enter keydown event for submitting source control push modal * quicksave * user workflow table for export * improve telemetry data * pull api telemetry * fix lint * Copy tweaks. * remove authorName and authorEmail and pick from user * rename owners.json to workflow_owners.json * ignore credential conflicts on pull * feat: several push/pull flow changes and design update * pull and push return same data format * fix: add One last step toast for successful pull * feat: add up to date pull toast * fix: add proper Learn more link for push and pull modals * do not await tracking being sent * fix import * fix await * add more sourcecontrolfile status * Minor copy tweak for "More info". * Minor copy tweak for "More info". * ignore variable_stub conflicts on pull * ignore whitespace differences * do not show remote workflows that are not yet created * fix telemetry * fix toast when pulling deleted wf * lint fix * refactor and make some imports dynamic * fix variable edit validation * fix telemetry response * improve telemetry * fix unintenional delete commit * fix status unknown issue * fix up to date toast * do not export active state and reapply versionid * use update instead of upsert * fix: show all workflows when clicking push to git * feat: update Up to date pull translation * fix: update read only env checks * do not update versionid of only active flag changes * feat: prevent access to new workflow and templates import when read only env * feat: send only active state and version if workflow state is not dirty * fix: Detect when only active state has changed and prevent generation a new version ID * feat: improve readonly env messages * make getPreferences public * fix telemetry issue * fix: add partial workflow update based on dirty state when changing active state * update unit tests * fix: remove unsaved changes check in readOnlyEnv * fix: disable push to git button when read onyl env * fix: update readonly toast duration * fix: fix pinning and title input in protected mode * initial commit (NOT working) * working push * cleanup and implement pull * fix getstatus * update import to new method * var and tag diffs are no conflicts * only show pull conflict for workflows * refactor and ignore faulty credentials * add sanitycheck for missing git folder * prefer fetch over pull and limit depth to 1 * back to pull... * fix setting branch on initial connect * fix test * remove clean workfolder * refactor: Remove some unnecessary code * Fixed links to docs. * fix getstatus query params * lint fix * dialog to show local and remote name on conflict * only show remote name on conflict * fix credential expression export * fix: Broken test * dont show toast on pull with empty var/tags and refactor * apply frontend changes from old branch * fix tag with same name import * fix buttons shown for non instance owners * prepare local storage key for removal * refactor: Change wording on pushing and pulling * refactor: Change menu item * test: Fix broken test * Update packages/cli/src/environments/sourceControl/types/sourceControlPushWorkFolder.ts Co-authored-by: Iván Ovejero <ivov.src@gmail.com> --------- Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
committed by
GitHub
parent
bcfc5e717b
commit
fc7aa8bd66
@@ -12,10 +12,16 @@ import type {
|
||||
SimpleGitOptions,
|
||||
StatusResult,
|
||||
} from 'simple-git';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import type { SourceControlPreferences } from './types/sourceControlPreferences';
|
||||
import { SOURCE_CONTROL_DEFAULT_BRANCH, SOURCE_CONTROL_ORIGIN } from './constants';
|
||||
import {
|
||||
SOURCE_CONTROL_DEFAULT_BRANCH,
|
||||
SOURCE_CONTROL_DEFAULT_EMAIL,
|
||||
SOURCE_CONTROL_DEFAULT_NAME,
|
||||
SOURCE_CONTROL_ORIGIN,
|
||||
} from './constants';
|
||||
import { sourceControlFoldersExistCheck } from './sourceControlHelper.ee';
|
||||
import type { User } from '../../databases/entities/User';
|
||||
import { getInstanceOwner } from '../../UserManagement/UserManagementHelper';
|
||||
|
||||
@Service()
|
||||
export class SourceControlGitService {
|
||||
@@ -27,7 +33,7 @@ export class SourceControlGitService {
|
||||
* Run pre-checks before initialising git
|
||||
* Checks for existence of required binaries (git and ssh)
|
||||
*/
|
||||
preInitCheck(): boolean {
|
||||
private preInitCheck(): boolean {
|
||||
LoggerProxy.debug('GitService.preCheck');
|
||||
try {
|
||||
const gitResult = execSync('git --version', {
|
||||
@@ -80,6 +86,8 @@ export class SourceControlGitService {
|
||||
trimmed: false,
|
||||
};
|
||||
|
||||
const { simpleGit } = await import('simple-git');
|
||||
|
||||
this.git = simpleGit(this.gitOptions)
|
||||
// Tell git not to ask for any information via the terminal like for
|
||||
// example the username. As nobody will be able to answer it would
|
||||
@@ -92,7 +100,8 @@ export class SourceControlGitService {
|
||||
}
|
||||
if (!(await this.hasRemote(sourceControlPreferences.repositoryUrl))) {
|
||||
if (sourceControlPreferences.connected && sourceControlPreferences.repositoryUrl) {
|
||||
await this.initRepository(sourceControlPreferences);
|
||||
const user = await getInstanceOwner();
|
||||
await this.initRepository(sourceControlPreferences, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,9 +110,9 @@ export class SourceControlGitService {
|
||||
this.git = null;
|
||||
}
|
||||
|
||||
async checkRepositorySetup(): Promise<boolean> {
|
||||
private async checkRepositorySetup(): Promise<boolean> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (async)');
|
||||
}
|
||||
if (!(await this.git.checkIsRepo())) {
|
||||
return false;
|
||||
@@ -116,9 +125,9 @@ export class SourceControlGitService {
|
||||
}
|
||||
}
|
||||
|
||||
async hasRemote(remote: string): Promise<boolean> {
|
||||
private async hasRemote(remote: string): Promise<boolean> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (async)');
|
||||
}
|
||||
try {
|
||||
const remotes = await this.git.getRemotes(true);
|
||||
@@ -139,11 +148,12 @@ export class SourceControlGitService {
|
||||
async initRepository(
|
||||
sourceControlPreferences: Pick<
|
||||
SourceControlPreferences,
|
||||
'repositoryUrl' | 'authorEmail' | 'authorName' | 'branchName' | 'initRepo'
|
||||
'repositoryUrl' | 'branchName' | 'initRepo'
|
||||
>,
|
||||
user: User,
|
||||
): Promise<void> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (Promise)');
|
||||
}
|
||||
if (sourceControlPreferences.initRepo) {
|
||||
try {
|
||||
@@ -161,8 +171,10 @@ export class SourceControlGitService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await this.git.addConfig('user.email', sourceControlPreferences.authorEmail);
|
||||
await this.git.addConfig('user.name', sourceControlPreferences.authorName);
|
||||
await this.setGitUserDetails(
|
||||
`${user.firstName} ${user.lastName}` ?? SOURCE_CONTROL_DEFAULT_NAME,
|
||||
user.email ?? SOURCE_CONTROL_DEFAULT_EMAIL,
|
||||
);
|
||||
if (sourceControlPreferences.initRepo) {
|
||||
try {
|
||||
const branches = await this.getBranches();
|
||||
@@ -175,9 +187,17 @@ export class SourceControlGitService {
|
||||
}
|
||||
}
|
||||
|
||||
async setGitUserDetails(name: string, email: string): Promise<void> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized (setGitUserDetails)');
|
||||
}
|
||||
await this.git.addConfig('user.email', name);
|
||||
await this.git.addConfig('user.name', email);
|
||||
}
|
||||
|
||||
async getBranches(): Promise<{ branches: string[]; currentBranch: string }> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (getBranches)');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -200,23 +220,16 @@ export class SourceControlGitService {
|
||||
|
||||
async setBranch(branch: string): Promise<{ branches: string[]; currentBranch: string }> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (setBranch)');
|
||||
}
|
||||
await this.git.checkout(branch);
|
||||
await this.git.branch([`--set-upstream-to=${SOURCE_CONTROL_ORIGIN}/${branch}`, branch]);
|
||||
return this.getBranches();
|
||||
}
|
||||
|
||||
async fetch(): Promise<FetchResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
}
|
||||
return this.git.fetch();
|
||||
}
|
||||
|
||||
async getCurrentBranch(): Promise<{ current: string; remote: string }> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (getCurrentBranch)');
|
||||
}
|
||||
const currentBranch = (await this.git.branch()).current;
|
||||
return {
|
||||
@@ -225,49 +238,47 @@ export class SourceControlGitService {
|
||||
};
|
||||
}
|
||||
|
||||
async diff(options?: { target?: string; dots?: '..' | '...' }): Promise<DiffResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
}
|
||||
const currentBranch = await this.getCurrentBranch();
|
||||
const target = options?.target ?? currentBranch.remote;
|
||||
const dots = options?.dots ?? '...';
|
||||
return this.git.diffSummary([dots + target]);
|
||||
}
|
||||
|
||||
async diffRemote(): Promise<DiffResult | undefined> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (diffRemote)');
|
||||
}
|
||||
const currentBranch = await this.getCurrentBranch();
|
||||
if (currentBranch.remote) {
|
||||
const target = currentBranch.remote;
|
||||
return this.git.diffSummary(['...' + target]);
|
||||
return this.git.diffSummary(['...' + target, '--ignore-all-space']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async diffLocal(): Promise<DiffResult | undefined> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (diffLocal)');
|
||||
}
|
||||
const currentBranch = await this.getCurrentBranch();
|
||||
if (currentBranch.remote) {
|
||||
const target = currentBranch.current;
|
||||
return this.git.diffSummary([target]);
|
||||
return this.git.diffSummary([target, '--ignore-all-space']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async fetch(): Promise<FetchResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized (fetch)');
|
||||
}
|
||||
return this.git.fetch();
|
||||
}
|
||||
|
||||
async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (pull)');
|
||||
}
|
||||
const params = {};
|
||||
if (options.ffOnly) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
return this.git.pull(undefined, undefined, { '--ff-only': null });
|
||||
Object.assign(params, { '--ff-only': true });
|
||||
}
|
||||
return this.git.pull();
|
||||
return this.git.pull(params);
|
||||
}
|
||||
|
||||
async push(
|
||||
@@ -278,7 +289,7 @@ export class SourceControlGitService {
|
||||
): Promise<PushResult> {
|
||||
const { force, branch } = options;
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized ({)');
|
||||
}
|
||||
if (force) {
|
||||
return this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']);
|
||||
@@ -288,7 +299,7 @@ export class SourceControlGitService {
|
||||
|
||||
async stage(files: Set<string>, deletedFiles?: Set<string>): Promise<string> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (stage)');
|
||||
}
|
||||
if (deletedFiles?.size) {
|
||||
try {
|
||||
@@ -301,10 +312,10 @@ export class SourceControlGitService {
|
||||
}
|
||||
|
||||
async resetBranch(
|
||||
options: { hard?: boolean; target: string } = { hard: false, target: 'HEAD' },
|
||||
options: { hard: boolean; target: string } = { hard: true, target: 'HEAD' },
|
||||
): Promise<string> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (Promise)');
|
||||
}
|
||||
if (options?.hard) {
|
||||
return this.git.raw(['reset', '--hard', options.target]);
|
||||
@@ -316,14 +327,14 @@ export class SourceControlGitService {
|
||||
|
||||
async commit(message: string): Promise<CommitResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (commit)');
|
||||
}
|
||||
return this.git.commit(message);
|
||||
}
|
||||
|
||||
async status(): Promise<StatusResult> {
|
||||
if (!this.git) {
|
||||
throw new Error('Git is not initialized');
|
||||
throw new Error('Git is not initialized (status)');
|
||||
}
|
||||
const statusResult = await this.git.status();
|
||||
return statusResult;
|
||||
|
||||
Reference in New Issue
Block a user