Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import { getBranches as gitGetBranches } from "../core/for-each-ref";
import { Branch } from "../core/models/branch";
import { sortBy, groupBy } from "underscore";
import { deleteRef } from "../core/update-ref";
import { pushDelete } from "../core/push";
import { GitActionError, GitActionErrorCode } from "./git-action-error";
export async function getBranches(
repositoryDir: string
): Promise<readonly Branch[]> {
const branches = await gitGetBranches(repositoryDir);
const groupped = groupBy(branches, (x) => x.type);
Object.keys(groupped).forEach((key) => sortBy(groupped[key], (x) => x.name));
return Object.keys(groupped).flatMap((key) => groupped[key]);
}
/**
* Delete a local branch, this will leave the remote branch intact if there is one.
*
* @param repositoryDir
* @param branch
*/
export async function deleteLocalBranch(
repositoryDir: string,
branch: Branch
): Promise<void> {
await deleteRef(repositoryDir, branch.ref);
}
/**
* Delete a remote branch.
*
* @param repositoryDir
* @param branch
*/
export async function deleteRemoteBranch(
repositoryDir: string,
branch: Branch
): Promise<void> {
if (!branch.upstream) {
throw new Error("Branch is not remote.");
}
try {
await pushDelete(
repositoryDir,
branch.upstreamRemoteName,
branch.nameWithoutRemote
);
} catch (error) {
const message = error.toString();
if (message.includes("remote ref does not exist")) {
throw new GitActionError(GitActionErrorCode.BranchNotExisting);
} else {
throw error;
}
}
}

View File

@@ -0,0 +1,17 @@
import { fetch as gitFetch } from '../core/fetch';
import { IGitResult } from '../core/git-error';
import { IFetchProgress } from '../core/models/progress';
import { IRemote } from '../core/models/remote';
import { createErrorFromMessage } from './git-action-error';
export async function fetch(
repositoryDir: string,
remote: IRemote,
progressCallback?: (progress: IFetchProgress) => void
): Promise<IGitResult> {
try {
return await gitFetch(repositoryDir, remote, progressCallback);
} catch (error) {
throw createErrorFromMessage(error.toString());
}
}

View File

@@ -0,0 +1,48 @@
export enum GitActionErrorCode {
LocalRepository,
InvalidBranchName,
BranchNotExisting,
StashNoLocalChanges,
AuthorizationFailed
}
/**
* GitActionError will be thrown from the action functions,
* which are designed to be a higher level of interaction
* with git than the other functions.
*/
export class GitActionError extends Error {
public constructor(public readonly code: GitActionErrorCode) {
super(getMessage(code));
}
}
function getMessage(code: GitActionErrorCode): string {
switch (code) {
case GitActionErrorCode.LocalRepository:
return 'Repository is not published.';
case GitActionErrorCode.InvalidBranchName:
return 'Branch name contains invalid characters.';
case GitActionErrorCode.BranchNotExisting:
return 'Branch does not exist.';
case GitActionErrorCode.StashNoLocalChanges:
return 'No local changes to save.';
case GitActionErrorCode.AuthorizationFailed:
return 'Authorization failed.';
default:
return String(code);
}
}
export function createErrorFromMessage(message: string) {
if (message.includes('Authentication failed')) {
return new GitActionError(GitActionErrorCode.AuthorizationFailed);
} else {
return new Error(message);
}
}

View File

@@ -0,0 +1,87 @@
import { getCommits } from "../core/logs";
import { Branch } from "../core/models/branch";
import { Commit } from "../core/models/snapshot";
import { getAheadBehind, revSymmetricDifference } from "../core/rev-list";
export class CommitHistoryEntry extends Commit {
public isLocalAhead: boolean;
public isRemoteAhead: boolean;
public constructor(
commit: Commit,
isLocalAhead: boolean,
isRemoteAhead: boolean
) {
super(
commit.repositoryDir,
commit.sha,
commit.shortSha,
commit.summary,
commit.body,
commit.author,
commit.committer,
commit.parentSHAs,
commit.tags
);
this.isLocalAhead = isLocalAhead;
this.isRemoteAhead = isRemoteAhead;
}
}
/**
* Class designed to handle history between multiple branches.
*/
export class CommitHistory {
public constructor(
public readonly repositoryDir: string,
public readonly branch: Branch
) {}
public async fetch(count: number): Promise<readonly CommitHistoryEntry[]> {
const localGitCommits = await getCommits(
this.repositoryDir,
undefined,
count
);
let commits: CommitHistoryEntry[] = localGitCommits.map(
(x) => new CommitHistoryEntry(x, false, false)
);
if (this.branch.remote) {
const remoteAheadGitCommits = await getCommits(
this.repositoryDir,
`${this.branch.nameWithoutRemote}..${this.branch.remote.name}`,
count
);
const remoteOnlyCommits = remoteAheadGitCommits.map(
(x) => new CommitHistoryEntry(x, false, true)
);
//get commits that aren't pushed
const localAheadGitCommits = await getCommits(
this.repositoryDir,
`${this.branch.remote.name}..${this.branch.nameWithoutRemote}`,
count
);
localAheadGitCommits.forEach((aheadCommit) => {
const c = commits.find((c) => c.sha === aheadCommit.sha);
if (c) {
c.isLocalAhead = true;
}
});
commits = remoteOnlyCommits.concat(commits);
} else {
// there is no remote, it's a local branch
// flag all the commits as being "ahead" of
// the remote (even the commit that was the branching points, and commits before it)
commits.forEach((c) => (c.isLocalAhead = true));
}
return commits;
}
}

View File

@@ -0,0 +1,7 @@
export * from './branch';
export * from './history';
export * from './git-action-error';
export * from './push';
export * from './remote';
export * from './pull';
export * from './fetch';

View File

@@ -0,0 +1,18 @@
import { Branch } from '../core/models/branch';
import { IPullProgress } from '../core/models/progress';
import { IRemote } from '../core/models/remote';
import { pull as gitPull } from '../core/pull';
import { createErrorFromMessage, GitActionError, GitActionErrorCode } from './git-action-error';
export async function pull(
repositoryDir: string,
remote: IRemote,
branch: Branch | string,
progressCallback?: (progress: IPullProgress) => void
): Promise<void> {
try {
return await gitPull(repositoryDir, remote, branch, progressCallback);
} catch (error) {
throw createErrorFromMessage(error.toString());
}
}

View File

@@ -0,0 +1,37 @@
import { Branch } from '../core/models/branch';
import { IPushProgress } from '../core/models/progress';
import { push as gitPush } from '../core/push';
import { createErrorFromMessage, GitActionError, GitActionErrorCode } from './git-action-error';
import { getRemote } from './remote';
interface PushOptions {
baseDir: string;
currentBranch: Branch;
onProgress?: (progress: IPushProgress) => void;
}
export async function push({ baseDir, currentBranch, onProgress }: PushOptions): Promise<boolean> {
const remote = await getRemote(baseDir);
try {
return await gitPush(
baseDir,
remote,
currentBranch.nameWithoutRemote,
currentBranch.upstreamWithoutRemote,
[],
undefined,
onProgress
);
} catch (error) {
const message = error.toString();
if (message.includes('Updates were rejected because the remote contains work that you do')) {
throw new Error(
'Updates were rejected because there are new changes that you do not have locally. Pull to get the latest changes.'
);
}
throw createErrorFromMessage(error.toString());
}
}

View File

@@ -0,0 +1,30 @@
import { IRemote } from '../core/models/remote';
import { getRemotes, setRemoteURL as _setRemoteURL } from '../core/remotes';
import { GitActionError, GitActionErrorCode } from './git-action-error';
/**
* Returns a single remote.
*
* @param repositoryDir
* @throws {GitHelperError}
* @returns A single remote.
*/
export async function getRemote(repositoryDir: string): Promise<Readonly<IRemote>> {
const remotes = await getRemotes(repositoryDir);
if (remotes.length === 0) {
// When there are no remotes, we assume that the repository is local only.
// This might not always be the case,
// but ideally a remote branch should have been created.
throw new GitActionError(GitActionErrorCode.LocalRepository);
}
// TODO: It would be nice if the git client selects a default remote
// and then we work from that remote, so you can
// technically have many different remotes at once.
return remotes[0];
}
export async function setRemoteURL(repositoryDir: string, remoteName: string, url: string): Promise<void> {
await _setRemoteURL(repositoryDir, remoteName, url);
}