提交 d298248b 编写于 作者: P Peng Lyu

git refactor

上级 d1ddcb49
# Git Extended
# VSCode Pull Request Support for GitHub
Experimental enhanced Git/Review/PR experience.
## Notices
Uses code from github desktop: https://github.com/desktop/desktop
\ No newline at end of file
Experimental enhanced Git/Review/PR experience.
\ No newline at end of file
......@@ -8,8 +8,7 @@ import { getFileContent, writeTmpFile } from './file';
import { GitChangeType, RichFileChange } from './models/file';
import { Repository } from './models/repository';
import { Comment } from './models/comment';
import { DiffHunk } from './models/diffHunk';
import { getDiffChangeType, DiffLine, DiffChangeType } from './models/diffLine';
import { DiffHunk, getDiffChangeType, DiffLine, DiffChangeType } from './models/diffHunk';
export const MODIFY_DIFF_INFO = /diff --git a\/(\S+) b\/(\S+).*\n*index.*\n*-{3}.*\n*\+{3}.*\n*((.*\n*)+)/;
export const NEW_FILE_INFO = /diff --git a\/(\S+) b\/(\S+).*\n*new file mode .*\nindex.*\n*-{3}.*\n*\+{3}.*\n*((.*\n*)+)/;
......@@ -53,44 +52,47 @@ export function* parseDiffHunk(diffHunkPatch: string): IterableIterator<DiffHunk
let itr = lineReader.next();
let diffHunk: DiffHunk = null;
let diffLine = -1;
let positionInHunk = -1;
let oldLine = -1;
let newLine = -1;
while (!itr.done) {
let line = itr.value;
const line = itr.value;
if (DIFF_HUNK_HEADER.test(line)) {
if (diffHunk) {
yield diffHunk;
diffHunk = null;
}
if (diffLine === -1) {
diffLine = 0;
if (positionInHunk === -1) {
positionInHunk = 0;
}
let matches = DIFF_HUNK_HEADER.exec(line);
let oriStartLine = oldLine = Number(matches[1]);
let oriLen = Number(matches[3]) | 0;
let newStartLine = newLine = Number(matches[5]);
let newLen = Number(matches[7]) | 0;
const matches = DIFF_HUNK_HEADER.exec(line);
const oriStartLine = oldLine = Number(matches[1]);
const oriLen = Number(matches[3]) | 0;
const newStartLine = newLine = Number(matches[5]);
const newLen = Number(matches[7]) | 0;
diffHunk = new DiffHunk(oriStartLine, oriLen, newStartLine, newLen, diffLine);
diffHunk = new DiffHunk(oriStartLine, oriLen, newStartLine, newLen, positionInHunk);
} else if (diffHunk !== null) {
let type = getDiffChangeType(line[0]);
let type = getDiffChangeType(line);
if (type !== DiffChangeType.Control) {
diffHunk.Lines.push(new DiffLine(type, type !== DiffChangeType.Add ? oldLine : -1,
if (type === DiffChangeType.Control) {
if (diffHunk.diffLines && diffHunk.diffLines.length) {
diffHunk.diffLines[diffHunk.diffLines.length - 1].endwithLineBreak = false;
}
} else {
diffHunk.diffLines.push(new DiffLine(type, type !== DiffChangeType.Add ? oldLine : -1,
type !== DiffChangeType.Delete ? newLine : -1,
diffLine,
positionInHunk,
line
));
var lineCount = 1;
lineCount += countCarriageReturns(line);
let lineCount = 1 + countCarriageReturns(line);
switch (type) {
case DiffChangeType.None:
case DiffChangeType.Context:
oldLine += lineCount;
newLine += lineCount;
break;
......@@ -103,8 +105,9 @@ export function* parseDiffHunk(diffHunkPatch: string): IterableIterator<DiffHunk
}
}
}
if (diffLine !== -1) {
++diffLine;
if (positionInHunk !== -1) {
++positionInHunk;
}
itr = lineReader.next();
}
......@@ -120,9 +123,9 @@ export function getDiffLineByPosition(prPatch: string, diffLineNumber: number):
while (!prDiffIter.done) {
let diffHunk = prDiffIter.value;
for (let i = 0; i < diffHunk.Lines.length; i++) {
if (diffHunk.Lines[i].diffLineNumber === diffLineNumber) {
return diffHunk.Lines[i];
for (let i = 0; i < diffHunk.diffLines.length; i++) {
if (diffHunk.diffLines[i].positionInHunk === diffLineNumber) {
return diffHunk.diffLines[i];
}
}
......@@ -159,7 +162,7 @@ export function mapHeadLineToDiffHunkPosition(prPatch: string, localDiff: string
while (!prDiffIter.done) {
let diffHunk = prDiffIter.value;
if (diffHunk.newLineNumber <= lineInPRDiff && diffHunk.newLineNumber + diffHunk.newLength - 1 >= lineInPRDiff) {
positionInDiffHunk = lineInPRDiff - diffHunk.newLineNumber + diffHunk.diffLine + 1;
positionInDiffHunk = lineInPRDiff - diffHunk.newLineNumber + diffHunk.positionInHunk + 1;
break;
}
......@@ -209,13 +212,13 @@ async function parseModifiedHunkComplete(originalContent, patch, a, b) {
lastCommonLine = oriStartLine + diffHunk.oldLength - 1;
for (let j = 0; j < diffHunk.Lines.length; j++) {
let diffLine = diffHunk.Lines[j];
for (let j = 0; j < diffHunk.diffLines.length; j++) {
let diffLine = diffHunk.diffLines[j];
if (diffLine.type === DiffChangeType.Delete) {
} else if (diffLine.type === DiffChangeType.Add) {
right.push(diffLine.content.substr(1));
right.push(diffLine.text);
} else {
let codeInFirstLine = diffLine.content.substr(1);
let codeInFirstLine = diffLine.text;
right.push(codeInFirstLine);
}
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from github/desktop, obtained from
* https://github.com/desktop/desktop/blob/0ce0de27f4eff526c073a159ce36ac9b27407e5c/app/src/lib/git/log.ts
* ------------------------------------------------------------------------------------------ */
import { Repository } from './models/repository';
import { Commit } from './models/commit';
import { GitProcess } from 'dugite';
export async function getParentCommit(repository: Repository, sha: string): Promise<string> {
const result = await GitProcess.exec(
[
'rev-list',
'--parents',
'-n',
'1',
sha
],
repository.path
);
let commits = result.stdout.split(' ');
if (commits.length > 2) {
return commits[1];
} else {
return null;
}
}
export async function getCommits(repository: Repository, revisionRange: string, limit: number, additionalArgs: ReadonlyArray<string> = []): Promise<Commit[]> {
const delimiter = '1F';
const delimiterString = String.fromCharCode(parseInt(delimiter, 16));
const prettyFormat = [
'%H', // SHA
'%s', // summary
'%P', // parent SHAs
].join(`%x${delimiter}`);
const result = await GitProcess.exec([
'log',
revisionRange,
`--max-count=${limit}`,
`--pretty=${prettyFormat}`,
'-z',
...additionalArgs,
], repository.path);
const out = result.stdout;
const lines = out.split('\0');
lines.splice(-1, 1);
const commits = lines.map(line => {
const pieces = line.split(delimiterString);
const sha = pieces[0];
const summary = pieces[1];
const shaList = pieces[2];
const parentSHAs = shaList.length ? shaList.split(' ') : [];
return new Commit(sha, summary, parentSHAs);
});
return commits;
}
export async function isWorkingTreeClean(repository: Repository): Promise<boolean> {
const result = await GitProcess.exec(
[
'diff-index',
'--quiet',
'HEAD',
'--'
],
repository.path
);
let exitCode = result.exitCode;
if (exitCode !== 0) {
return false;
} else {
return true;
}
}
\ No newline at end of file
......@@ -3,16 +3,51 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DiffLine } from './diffLine';
export enum DiffChangeType {
Context,
Add,
Delete,
Control
}
export class DiffLine {
public get raw(): string {
return this._raw;
}
public get text(): string {
return this._raw.substr(1);
}
public endwithLineBreak: boolean = true;
constructor(
public type: DiffChangeType,
public oldLineNumber: number, /* 1 based */
public newLineNumber: number, /* 1 based */
public positionInHunk: number,
private _raw: string
) { }
}
export function getDiffChangeType(text: string) {
let c = text[0];
switch (c) {
case ' ': return DiffChangeType.Context;
case '+': return DiffChangeType.Add;
case '-': return DiffChangeType.Delete;
case '\\': return DiffChangeType.Control;
}
}
export class DiffHunk {
public Lines: DiffLine[] = [];
public diffLines: DiffLine[] = [];
constructor(
public oldLineNumber: number,
public oldLength: number,
public newLineNumber: number,
public newLength: number,
public diffLine: number
public positionInHunk: number
) { }
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from github/VisualStudio project, obtained from
* https://github.com/github/VisualStudio/blob/master/src/GitHub.Exports/Models/DiffLine.cs
* ------------------------------------------------------------------------------------------ */
export enum DiffChangeType {
None,
Add,
Delete,
Control
}
export class DiffLine {
// Was the line added, deleted or unchanged.
type: DiffChangeType;
// Gets the old 1-based line number.
oldLineNumber: number = -1;
// Gets the new 1-based line number.
newLineNumber: number = -1;
// Gets the unified diff line number where the first chunk header is line 0.
diffLineNumber: number = -1;
// Gets the content of the diff line (including +, - or space).
content: String;
constructor(type: DiffChangeType, oldLineNumber: number, newLineNumber: number, diffLineNumber: number, content: string) {
this.type = type;
this.oldLineNumber = oldLineNumber;
this.newLineNumber = newLineNumber;
this.diffLineNumber = diffLineNumber;
this.content = content;
}
}
export function getDiffChangeType(text: string) {
let c = text[0];
switch (c) {
case ' ': return DiffChangeType.None;
case '+': return DiffChangeType.Add;
case '-': return DiffChangeType.Delete;
case '\\': return DiffChangeType.Control;
}
}
\ No newline at end of file
......@@ -3,16 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriString } from './uriString';
import { Protocol } from './protocol';
export class GitReferenceModel {
public repositoryCloneUrl: UriString;
export class GitHubRef {
public repositoryCloneUrl: Protocol;
constructor(
public ref: string,
public label: string,
public sha: string,
repositoryCloneUrl: string
) {
this.repositoryCloneUrl = new UriString(repositoryCloneUrl);
this.repositoryCloneUrl = new Protocol(repositoryCloneUrl);
}
}
\ No newline at end of file
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Repository } from './repository';
export class Model {
constructor(repository: Repository) {
}
}
\ No newline at end of file
......@@ -3,22 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from github/VisualStudio project, obtained from
* https://github.com/github/VisualStudio/blob/master/src/GitHub.Exports/Primitives/UriString.cs
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
const sshRegex = /^.+@(([.*?]|[a-z0-9-.]+?))(:(.*?))?(\/(.*)(.git)?)?$/i;
export class UriString {
public host: string;
export enum ProtocolType {
Local,
HTTP,
SSH,
GIT,
OTHER
}
const gitProtocolRegex = [
new RegExp('^git@(.+):(.+)/(.+?)(?:/|.git)?$'),
new RegExp('^git:(.+)/(.+)/(.+?)(?:/|.git)?$')
];
const sshProtocolRegex = [
new RegExp('^ssh://git@(.+)/(.+)/(.+?)(?:/|.git)?$')
];
export class Protocol {
public type: ProtocolType = ProtocolType.OTHER;
public host: string = '';
public owner: string;
public owner: string = '';
public repositoryName: string;
public repositoryName: string = '';
public nameWithOwner: string;
public get nameWithOwner(): string {
return this.owner ? `${this.owner}/${this.repositoryName}` : this.repositoryName;
}
public isFileUri: boolean;
......@@ -30,56 +43,51 @@ export class UriString {
constructor(
uriString: string
) {
let parseUriSuccess = false;
try {
this.url = vscode.Uri.parse(uriString);
parseUriSuccess = true;
if (this.url.scheme === 'file') {
this.setFilePath(this.url);
} else {
this.setUri(this.url);
this.type = ProtocolType.Local;
this.repositoryName = this.getRepositoryName(this.url.path);
return;
}
if (this.url.scheme === 'https' || this.url.scheme === 'http') {
this.type = ProtocolType.HTTP;
this.host = this.url.authority;
this.repositoryName = this.getRepositoryName(this.url.path);
this.owner = this.getOwnerName(this.url.path);
return;
}
} catch (e) { }
if (!parseUriSuccess) {
try {
let matches = sshRegex.exec(uriString);
if (matches) {
this.host = matches[1];
this.owner = matches[4];
this.repositoryName = this.getRepositoryName(matches[6]);
this.isScpUri = true;
} else {
this.setFilePath2(uriString);
try {
for (const regex of gitProtocolRegex) {
const result = uriString.match(regex);
if (!result) {
continue;
}
} catch (e) { }
}
if (this.repositoryName) {
this.nameWithOwner = this.owner ? `${this.owner}/${this.repositoryName}` : this.repositoryName;
}
}
setUri(uri: vscode.Uri) {
this.host = uri.authority;
this.repositoryName = this.getRepositoryName(uri.path);
this.owner = this.getOwnerName(uri.path);
}
this.host = result[1];
this.owner = result[2];
this.repositoryName = result[3];
this.type = ProtocolType.GIT;
return;
}
setFilePath(uri: vscode.Uri) {
this.host = '';
this.owner = '';
this.repositoryName = this.getRepositoryName(uri.path);
this.isFileUri = true;
}
for (const regex of sshProtocolRegex) {
const result = uriString.match(regex);
if (!result) {
continue;
}
setFilePath2(path: string) {
this.host = '';
this.owner = '';
this.repositoryName = this.getRepositoryName(path);
this.isFileUri = true;
this.host = result[1];
this.owner = result[2];
this.repositoryName = result[3];
this.type = ProtocolType.SSH;
return;
}
} catch (e) { }
}
getRepositoryName(path: string) {
......@@ -103,8 +111,12 @@ export class UriString {
return null;
}
toRepositoryUrl(owner: string = null): vscode.Uri {
if (!this.isScpUri && (!this.url || this.isFileUri)) {
normalizeUri(): vscode.Uri {
if (this.type === ProtocolType.OTHER && !this.url) {
return null;
}
if (this.isFileUri) {
return this.url;
}
......@@ -113,16 +125,14 @@ export class UriString {
scheme = this.url.scheme;
}
let nameWithOwner = this.owner ? `${this.owner}/${this.repositoryName}` : this.repositoryName;
try {
return vscode.Uri.parse(`${scheme}://${this.host}/${nameWithOwner}`);
return vscode.Uri.parse(`${scheme}://${this.host}/${this.nameWithOwner}`);
} catch (e) {
return null;
}
}
equals(other: UriString) {
return this.toRepositoryUrl().toString().toLocaleLowerCase() === other.toRepositoryUrl().toString().toLocaleLowerCase();
equals(other: Protocol) {
return this.normalizeUri().toString().toLocaleLowerCase() === other.normalizeUri().toString().toLocaleLowerCase();
}
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ import { Remote } from './remote';
import { parseComments } from '../comment';
import { Comment } from './comment';
import { IAccount } from './account';
import { GitReferenceModel } from './gitReferenceModel';
import { GitHubRef } from './githubRef';
export enum PRType {
RequestReview = 0,
......@@ -43,8 +43,8 @@ export class PullRequestModel {
return this.state === PullRequestStateEnum.Merged;
}
public head: GitReferenceModel;
public base: GitReferenceModel;
public head: GitHubRef;
public base: GitHubRef;
constructor(public readonly otcokit: any, public readonly remote: Remote, public prItem: any) {
this.prNumber = prItem.number;
......@@ -81,14 +81,14 @@ export class PullRequestModel {
this.commentCount = prItem.comments;
this.commitCount = prItem.commits;
this.head = new GitReferenceModel(prItem.head.ref, prItem.head.label, prItem.head.sha, prItem.head.repo.clone_url);
this.base = new GitReferenceModel(prItem.base.ref, prItem.base.label, prItem.base.sha, prItem.base.repo.clone_url);
this.head = new GitHubRef(prItem.head.ref, prItem.head.label, prItem.head.sha, prItem.head.repo.clone_url);
this.base = new GitHubRef(prItem.base.ref, prItem.base.label, prItem.base.sha, prItem.base.repo.clone_url);
}
async getFiles() {
const { data } = await this.otcokit.pullRequests.getFiles({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: this.prItem.number
});
......@@ -100,7 +100,7 @@ export class PullRequestModel {
// this one is from search results, which is not complete.
const { data } = await this.otcokit.pullRequests.get({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: this.prItem.number
});
this.prItem = data;
......@@ -112,7 +112,7 @@ export class PullRequestModel {
async getComments(): Promise<Comment[]> {
const reviewData = await this.otcokit.pullRequests.getComments({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: this.prItem.number,
per_page: 100
});
......@@ -123,7 +123,7 @@ export class PullRequestModel {
async createCommentReply(body: string, reply_to: string) {
let ret = await this.otcokit.pullRequests.createCommentReply({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: this.prItem.number,
body: body,
in_reply_to: reply_to
......@@ -135,7 +135,7 @@ export class PullRequestModel {
async createComment(body: string, path: string, position: number) {
let ret = await this.otcokit.pullRequests.createComment({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: this.prItem.number,
body: body,
commit_id: this.prItem.head.sha,
......
......@@ -3,29 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Protocol } from './protocol';
export class Remote {
public get host(): string {
return this.gitProtocol.host;
}
public get owner(): string {
return this.gitProtocol.owner;
}
public get repositoryName(): string {
return this.gitProtocol.repositoryName;
}
constructor(
public readonly remoteName: string,
public readonly url: string,
public readonly hostname: string,
public readonly owner: string,
public readonly name: string
public readonly gitProtocol: Protocol,
) { }
equals(remote: Remote): boolean {
if (this.remoteName !== remote.remoteName) {
return false;
}
// if (this.url !== remote.url) {
// return false;
// }
if (this.hostname !== remote.hostname) {
if (this.host !== remote.host) {
return false;
}
if (this.owner !== remote.owner) {
return false;
}
if (this.name !== remote.name) {
if (this.repositoryName !== remote.repositoryName) {
return false;
}
......
......@@ -7,11 +7,11 @@ import * as vscode from 'vscode';
import { Remote } from './remote';
import { GitProcess } from 'dugite';
import { uniqBy, anyEvent, filterEvent, isDescendant } from '../util';
import { parseRemote } from '../remote';
import { CredentialStore } from '../../credentials';
import { PullRequestModel, PRType } from './pullRequestModel';
import { UriString } from './uriString';
import { Protocol } from './protocol';
import { GitError, GitErrorCodes } from './gitError';
import { PullRequestGitHelper } from '../pullRequestGitHelper';
export enum RefType {
Head,
......@@ -43,7 +43,7 @@ export class Repository {
private _onDidRunGitStatus = new vscode.EventEmitter<void>();
readonly onDidRunGitStatus: vscode.Event<void> = this._onDidRunGitStatus.event;
public githubRepositories?: GitHubRepository[];
public githubRepositories?: GitHubRepository[] = [];
private _HEAD: Branch | undefined;
get HEAD(): Branch | undefined {
......@@ -61,8 +61,8 @@ export class Repository {
}
// todo
private _cloneUrl: UriString;
get cloneUrl(): UriString {
private _cloneUrl: Protocol;
get cloneUrl(): Protocol {
return this._cloneUrl;
}
......@@ -116,7 +116,7 @@ export class Repository {
if (this._HEAD.upstream && this._HEAD.upstream.remote) {
let currentRemote = this._remotes.filter(remote => remote.remoteName === this._HEAD.upstream.remote);
if (currentRemote && currentRemote.length) {
this._cloneUrl = new UriString(currentRemote[0].url);
this._cloneUrl = new Protocol(currentRemote[0].url);
}
}
......@@ -126,7 +126,13 @@ export class Repository {
async connectGitHub(credentialStore: CredentialStore) {
let ret: GitHubRepository[] = [];
await Promise.all(this.remotes.map(async remote => {
let isRemoteForPR = await PullRequestGitHelper.isRemoteCreatedForPullRequest(this, remote.remoteName);
if (isRemoteForPR) {
return;
}
let octo = await credentialStore.getOctokit(remote);
if (octo) {
ret.push(new GitHubRepository(remote, octo));
}
......@@ -135,12 +141,12 @@ export class Repository {
this.githubRepositories = ret;
}
async fetch(remoteName: string, branch: string) {
async fetch(remoteName: string, branch?: string) {
const result = await GitProcess.exec(
[
'fetch',
remoteName,
branch
branch ? branch : ''
],
this.path
);
......@@ -375,7 +381,7 @@ export class GitHubRepository {
if (prType === PRType.All) {
let result = await this.octokit.pullRequests.getAll({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
});
let ret = result.data.map(item => {
if (!item.head.repo) {
......@@ -389,7 +395,7 @@ export class GitHubRepository {
const user = await this.octokit.users.get();
const { data } = await this.octokit.search.issues({
q: this.getPRFetchQuery(this.remote.owner, this.remote.name, user.data.login, prType)
q: this.getPRFetchQuery(this.remote.owner, this.remote.repositoryName, user.data.login, prType)
});
let promises = [];
......@@ -397,7 +403,7 @@ export class GitHubRepository {
promises.push(new Promise(async (resolve, reject) => {
let prData = await this.octokit.pullRequests.get({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: item.number
});
resolve(prData);
......@@ -419,7 +425,7 @@ export class GitHubRepository {
async getPullRequest(id: number) {
let { data } = await this.octokit.pullRequests.get({
owner: this.remote.owner,
repo: this.remote.name,
repo: this.remote.repositoryName,
number: id
});
if (!data.head.repo) {
......@@ -453,4 +459,14 @@ export class GitHubRepository {
return `is:open ${filter} type:pr repo:${owner}/${repo}`;
}
}
\ No newline at end of file
}
function parseRemote(remoteName: string, url: string): Remote | null {
let gitProtocol = new Protocol(url);
if (gitProtocol.host) {
return new Remote(remoteName, url, gitProtocol);
}
return null;
}
......@@ -3,143 +3,110 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from github/VisualStudio project, obtained from
* https://github.com/github/VisualStudio/blob/master/src/GitHub.App/Services/PullRequestService.cs
* ------------------------------------------------------------------------------------------ */
import { Repository } from '../common/models/repository';
import { PullRequestModel } from '../common/models/pullRequestModel';
import { UriString } from '../common/models/uriString';
import { Protocol } from '../common/models/protocol';
import { Remote } from './models/remote';
const InvalidBranchCharsRegex = /[^0-9A-Za-z\-]/g;
const SettingCreatedByGHfVSC = 'created-by-ghfvsc';
const SettingGHfVSCPullRequest = 'ghfvs-pr-owner-number';
const BranchCapture = /branch\.(.+)\.ghfvsc-pr/;
const PullRequestRemoteMetadataKey = 'github-pr-remote';
const PullRequestMetadataKey = 'github-pr-owner-number';
const PullRequestBranchRegex = /branch\.(.+)\.github-pr-owner-number/;
export class PullRequestGitHelper {
static async checkout(repository: Repository, pullRequest: PullRequestModel, localBranchName: string) {
static async createAndCheckout(repository: Repository, pullRequest: PullRequestModel) {
let localBranchName = await PullRequestGitHelper.getBranchNameForPullRequest(repository, pullRequest);
let existing = await repository.getBranch(localBranchName);
if (existing) {
// already exist
await repository.checkout(localBranchName);
} else if (repository.cloneUrl.equals(pullRequest.head.repositoryCloneUrl)) {
// branch from the same repository
await repository.fetch('origin', localBranchName);
await repository.checkout(localBranchName);
} else {
// nothing matches
let refSpec = `${pullRequest.head.ref}:${localBranchName}`;
// the branch is from a fork
// create remote for this fork
let remoteName = await PullRequestGitHelper.createRemote(repository, pullRequest.head.repositoryCloneUrl);
await repository.fetch(remoteName, refSpec);
// fetch the branch
let ref = `${pullRequest.head.ref}:${localBranchName}`;
await repository.fetch(remoteName, ref);
await repository.checkout(localBranchName);
// set remote tracking branch for the local branch
await repository.setTrackingBranch(localBranchName, `refs/remotes/${remoteName}/${pullRequest.head.ref}`);
}
var prConfigKey = `branch.${localBranchName}.${SettingGHfVSCPullRequest}`;
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildGHfVSConfigKeyValue(pullRequest));
let prConfigKey = `branch.${localBranchName}.${PullRequestMetadataKey}`;
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest));
}
static async getLocalBranchesForPullRequest(repository: Repository, pullRequest: PullRequestModel): Promise<string[]> {
if (PullRequestGitHelper.isPullRequestFromRepository(repository, pullRequest)) {
return [pullRequest.head.ref];
} else {
let key = PullRequestGitHelper.buildGHfVSConfigKeyValue(pullRequest);
static async checkout(repository: Repository, remote: Remote, branchName: string, pullRequest: PullRequestModel): Promise<void> {
let remoteName = remote.remoteName;
await repository.fetch(remoteName);
let branch = await repository.getBranch(branchName);
let configs = await repository.getConfigs();
return configs.map(config => {
let matches = BranchCapture.exec(config.key);
if (matches && matches.length) {
return {
branch: matches[1],
value: config.value
};
} else {
return {
branch: null,
value: config.value
};
}
}).filter(c => c.branch && c.value === key).map(c => c.value);
if (!branch) {
await PullRequestGitHelper.fetchAndCreateBranch(repository, remote, branchName, pullRequest);
}
}
static async switchToBranch(repository: Repository, pullRequest: PullRequestModel): Promise<void> {
let matchingBranches = await PullRequestGitHelper.getLocalBranchesForPullRequest(repository, pullRequest);
if (matchingBranches && matchingBranches.length) {
let branchName = matchingBranches[0];
let remoteName = repository.HEAD.upstream.remote;
await repository.checkout(branchName);
await PullRequestGitHelper.markBranchAsPullRequest(repository, pullRequest, branchName);
}
if (!remoteName) {
return;
}
static async getBranchForPullRequestFromExistingRemotes(repository: Repository, pullRequest: PullRequestModel) {
let headRemote = PullRequestGitHelper.getHeadRemoteForPullRequest(repository, pullRequest);
if (headRemote) {
// the head of the PR is in this repository (not fork), we can just fetch
return {
remote: headRemote,
branch: pullRequest.head.ref
};
} else {
let key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest);
let configs = await repository.getConfigs();
await repository.fetch(remoteName, branchName);
let branch = null;
try {
branch = await repository.getBranch(branchName);
} catch (e) { }
if (!branch) {
const trackedBranchName = `refs/remotes/${remoteName}/${branchName}`;
const trackedBranch = await repository.getBranch(trackedBranchName);
if (trackedBranch) {
// create branch
await repository.createBranch(branchName, trackedBranch.commit);
await repository.setTrackingBranch(branchName, trackedBranchName);
} else {
throw new Error(`Could not find branch '${trackedBranchName}'.`);
let branchInfos = configs.map(config => {
let matches = PullRequestBranchRegex.exec(config.key);
return {
branch: matches && matches.length ? matches[1] : null,
value: config.value
};
}).filter(c => c.branch && c.value === key);
if (branchInfos && branchInfos.length) {
let remoteName = await repository.getConfig(`branch.${branchInfos[0].branch}.remote`);
let headRemote = repository.remotes.filter(remote => remote.remoteName === remoteName);
if (headRemote && headRemote.length) {
return {
remote: headRemote[0],
branch: branchInfos[0].branch
};
}
}
await repository.checkout(branchName);
await PullRequestGitHelper.markBranchAsPullRequest(repository, pullRequest, branchName);
return null;
}
}
static async getDefaultLocalBranchName(repository: Repository, pullRequestNumber: number, pullRequestTitle: string): Promise<string> {
let initial = 'pr/' + pullRequestNumber + '-' + PullRequestGitHelper.getSafeBranchName(pullRequestTitle);
let current = initial;
let index = 2;
static async fetchAndCreateBranch(repository: Repository, remote: Remote, branchName: string, pullRequest: PullRequestModel) {
let remoteName = remote.remoteName;
const trackedBranchName = `refs/remotes/${remoteName}/${branchName}`;
const trackedBranch = await repository.getBranch(trackedBranchName);
while (true) {
let currentBranch = await repository.getBranch(current);
if (currentBranch) {
current = initial + '-' + index++;
} else {
break;
}
}
return current.replace(/-*$/g, '');
}
static async getPullRequestForCurrentBranch(repository: Repository) {
let configKey = `branch.${repository.HEAD.name}.${SettingGHfVSCPullRequest}`;
let configValue = await repository.getConfig(configKey);
return PullRequestGitHelper.parseGHfVSConfigKeyValue(configValue);
}
static getSafeBranchName(name: string): string {
let before = name.replace(InvalidBranchCharsRegex, '-').replace(/-*$/g, '');
for (; ;) {
let after = before.replace('--', '-');
if (after === before) {
return before.toLocaleLowerCase();
}
before = after;
if (trackedBranch) {
// create branch
await repository.createBranch(branchName, trackedBranch.commit);
await repository.setTrackingBranch(branchName, trackedBranchName);
} else {
throw new Error(`Could not find branch '${trackedBranchName}'.`);
}
}
static buildGHfVSConfigKeyValue(pullRequest: PullRequestModel) {
static buildPullRequestMetadata(pullRequest: PullRequestModel) {
return pullRequest.base.repositoryCloneUrl.owner + '#' + pullRequest.prNumber;
}
static parseGHfVSConfigKeyValue(value: string) {
static parsePullRequestMetadata(value: string) {
if (value) {
let separator = value.indexOf('#');
if (separator !== -1) {
......@@ -158,23 +125,29 @@ export class PullRequestGitHelper {
return null;
}
static async createRemote(repository: Repository, cloneUrl: UriString) {
static async getMatchingPullRequestMetadataForBranch(repository: Repository, branchName: string) {
let configKey = `branch.${branchName}.${PullRequestMetadataKey}`;
let configValue = await repository.getConfig(configKey);
return PullRequestGitHelper.parsePullRequestMetadata(configValue);
}
static async createRemote(repository: Repository, cloneUrl: Protocol) {
let remotes = repository.remotes;
remotes.forEach(remote => {
if (new UriString(remote.url).equals(cloneUrl)) {
return remote.name;
if (new Protocol(remote.url).equals(cloneUrl)) {
return remote.repositoryName;
}
});
var remoteName = PullRequestGitHelper.createUniqueRemoteName(repository, cloneUrl.owner);
await repository.addRemote(remoteName, cloneUrl.toRepositoryUrl().toString());
await repository.setConfig(`remote.${remoteName}.${SettingCreatedByGHfVSC}`, 'true');
let remoteName = PullRequestGitHelper.getUniqueRemoteName(repository, cloneUrl.owner);
await repository.addRemote(remoteName, cloneUrl.normalizeUri().toString());
await repository.setConfig(`remote.${remoteName}.${PullRequestRemoteMetadataKey}`, 'true');
return remoteName;
}
static async isRemoteCreatedForPullRequest(repository: Repository, remoteName: string) {
let isForPR = await repository.getConfig(`remote.${remoteName}.${SettingCreatedByGHfVSC}`);
let isForPR = await repository.getConfig(`remote.${remoteName}.${PullRequestRemoteMetadataKey}`);
if (isForPR === 'true') {
return true;
......@@ -183,33 +156,50 @@ export class PullRequestGitHelper {
}
}
static createUniqueRemoteName(repository: Repository, name: string) {
{
var uniqueName = name;
var number = 1;
static async getBranchNameForPullRequest(repository: Repository, pullRequest: PullRequestModel): Promise<string> {
let branchName = `pr/${pullRequest.author.login}/${pullRequest.prNumber}`;
let result = branchName;
let number = 1;
while (repository.remotes.find(e => e.remoteName === uniqueName)) {
uniqueName = name + number++;
}
while (true) {
let currentBranch = await repository.getBranch(result);
return uniqueName;
if (currentBranch) {
result = branchName + '-' + number++;
} else {
break;
}
}
return result;
}
static isPullRequestFromRepository(repository: Repository, pullRequest: PullRequestModel): boolean {
return repository.cloneUrl && repository.cloneUrl.equals(pullRequest.head.repositoryCloneUrl);
static getUniqueRemoteName(repository: Repository, name: string) {
let uniqueName = name;
let number = 1;
while (repository.remotes.find(e => e.remoteName === uniqueName)) {
uniqueName = name + number++;
}
return uniqueName;
}
static async getPullRequestForBranch(repository: Repository, branchName: string) {
let prConfigKey = `branch.${branchName}.${SettingGHfVSCPullRequest}`;
let info = await repository.getConfig(prConfigKey);
static getHeadRemoteForPullRequest(repository: Repository, pullRequest: PullRequestModel): Remote {
let repos = repository.githubRepositories;
for (let i = 0; i < repos.length; i++) {
let remote = repos[i].remote;
if (remote.gitProtocol && remote.gitProtocol.equals(pullRequest.head.repositoryCloneUrl)) {
return remote;
}
}
return info;
return null;
}
static async markBranchAsPullRequest(repository: Repository, pullRequest: PullRequestModel, branchName: string) {
let prConfigKey = `branch.${branchName}.${SettingGHfVSCPullRequest}`;
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildGHfVSConfigKeyValue(pullRequest));
let prConfigKey = `branch.${branchName}.${PullRequestMetadataKey}`;
await repository.setConfig(prConfigKey, PullRequestGitHelper.buildPullRequestMetadata(pullRequest));
}
static async getLocalBranchesMarkedAsPullRequest(repository: Repository) {
......@@ -217,9 +207,9 @@ export class PullRequestGitHelper {
let ret = [];
for (let i = 0; i < branches.length; i++) {
let localInfo = await PullRequestGitHelper.getPullRequestForBranch(repository, branches[i]);
if (localInfo) {
ret.push(PullRequestGitHelper.parseGHfVSConfigKeyValue(localInfo));
let matchingPRMetadata = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(repository, branches[i]);
if (matchingPRMetadata) {
ret.push(matchingPRMetadata);
}
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from github/desktop, obtained from
* https://github.com/desktop/desktop/blob/master/app/src/lib/git/remote.ts
* ------------------------------------------------------------------------------------------ */
import { GitProcess } from 'dugite';
import { Remote } from './models/remote';
export async function getRemotes(
path: string
) {
const result = await GitProcess.exec(['remote', '-v'], path);
const output = result.stdout;
const lines = output.split('\n');
const remotes = lines
.filter(x => x.endsWith('(fetch)'))
.map(x => x.split(/\s+/))
.map(x => ({ name: x[0], url: x[1] }));
return remotes;
}
/** Parse the remote information from URL. */
export function parseRemote(remoteName: string, url: string): Remote | null {
// Examples:
// https://github.com/octocat/Hello-World.git
// https://github.com/octocat/Hello-World.git/
// git@github.com:octocat/Hello-World.git
// git:github.com/octocat/Hello-World.git
const regexes = [
new RegExp('^https?://(?:.+@)?(.+)/(.+)/(.+?)(?:/|.git/?)?$'),
new RegExp('^git@(.+):(.+)/(.+?)(?:/|.git)?$'),
new RegExp('^git:(.+)/(.+)/(.+?)(?:/|.git)?$'),
new RegExp('^ssh://git@(.+)/(.+)/(.+?)(?:/|.git)?$')
];
for (const regex of regexes) {
const result = url.match(regex);
if (!result) {
continue;
}
const hostname = result[1];
const owner = result[2];
const name = result[3];
if (hostname) {
return new Remote(remoteName, url, hostname, owner, name);
}
}
return null;
}
......@@ -21,7 +21,7 @@ export class CredentialStore {
return this.octokits[remote.url];
}
if (this.configuration.host === remote.hostname && this.configuration.accessToken) {
if (this.configuration.host === remote.host && this.configuration.accessToken) {
this.octokits[remote.url] = Octokit({});
this.octokits[remote.url].authenticate({
type: 'token',
......
......@@ -285,7 +285,7 @@ export class PRProvider implements vscode.TreeDataProvider<PRGroupTreeItem | Pul
async getComments(element: PullRequestModel): Promise<Comment[]> {
const reviewData = await element.otcokit.pullRequests.getComments({
owner: element.remote.owner,
repo: element.remote.name,
repo: element.remote.repositoryName,
number: element.prItem.number,
per_page: 100,
});
......
......@@ -113,14 +113,14 @@ export class ReviewManager implements vscode.DecorationProvider {
}
private async validateState() {
let localInfo = await PullRequestGitHelper.getPullRequestForCurrentBranch(this._repository);
let matchingPullRequestMetadata = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(this._repository, this._repository.HEAD.name);
if (!localInfo) {
if (!matchingPullRequestMetadata) {
this.clear(true);
return;
}
if (this._prNumber === localInfo.prNumber) {
if (this._prNumber === matchingPullRequestMetadata.prNumber) {
return;
}
......@@ -138,9 +138,11 @@ export class ReviewManager implements vscode.DecorationProvider {
// we switch to another PR, let's clean up first.
this.clear(false);
this._prNumber = localInfo.prNumber;
this._prNumber = matchingPullRequestMetadata.prNumber;
this._lastCommitSha = null;
let githubRepo = this._repository.githubRepositories.find(repo => repo.remote.owner.toLocaleLowerCase() === localInfo.owner.toLocaleLowerCase());
let githubRepo = this._repository.githubRepositories.find(repo =>
repo.remote.owner.toLocaleLowerCase() === matchingPullRequestMetadata.owner.toLocaleLowerCase()
);
if (!githubRepo) {
return; // todo, should show warning
......@@ -487,14 +489,12 @@ export class ReviewManager implements vscode.DecorationProvider {
this.statusBarItem.show();
try {
// todo, check if HEAD is dirty
let localBranches = await PullRequestGitHelper.getLocalBranchesForPullRequest(this._repository, pr);
let localBranchInfo = await PullRequestGitHelper.getBranchForPullRequestFromExistingRemotes(this._repository, pr);
if (localBranches.length > 0) {
await PullRequestGitHelper.switchToBranch(this._repository, pr);
if (localBranchInfo) {
await PullRequestGitHelper.checkout(this._repository, localBranchInfo.remote, localBranchInfo.branch, pr);
} else {
let branchName = await PullRequestGitHelper.getDefaultLocalBranchName(this._repository, pr.prNumber, pr.title);
await PullRequestGitHelper.checkout(this._repository, pr, branchName);
await PullRequestGitHelper.createAndCheckout(this._repository, pr);
}
} catch (e) {
if (e.gitErrorCode) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册