Browse Source

Fixes & improves rebase handling

main
Eric Amodio 1 year ago
parent
commit
54b2b16426
29 changed files with 729 additions and 478 deletions
  1. +24
    -6
      package.json
  2. +1
    -2
      src/commands/quickCommand.steps.ts
  3. +2
    -0
      src/constants.ts
  4. +25
    -17
      src/env/node/git/git.ts
  5. +134
    -64
      src/env/node/git/localGitProvider.ts
  6. +3
    -3
      src/env/node/git/shell.ts
  7. +12
    -7
      src/git/gitProvider.ts
  8. +15
    -2
      src/git/gitProviderService.ts
  9. +3
    -3
      src/git/models/rebase.ts
  10. +76
    -0
      src/plus/github/github.ts
  11. +35
    -3
      src/plus/github/githubGitProvider.ts
  12. +20
    -20
      src/system/promise.ts
  13. +1
    -0
      src/views/branchesView.ts
  14. +1
    -2
      src/views/commitsView.ts
  15. +1
    -2
      src/views/nodes/branchNode.ts
  16. +1
    -2
      src/views/nodes/branchesNode.ts
  17. +9
    -6
      src/views/nodes/commitNode.ts
  18. +54
    -38
      src/views/nodes/fileRevisionAsCommitNode.ts
  19. +43
    -27
      src/views/nodes/mergeConflictCurrentChangesNode.ts
  20. +55
    -0
      src/views/nodes/mergeConflictFilesNode.ts
  21. +59
    -40
      src/views/nodes/mergeConflictIncomingChangesNode.ts
  22. +27
    -38
      src/views/nodes/mergeStatusNode.ts
  23. +26
    -0
      src/views/nodes/rebaseCommitNode.ts
  24. +57
    -176
      src/views/nodes/rebaseStatusNode.ts
  25. +2
    -6
      src/views/nodes/statusFilesNode.ts
  26. +6
    -2
      src/views/nodes/viewNode.ts
  27. +1
    -0
      src/views/remotesView.ts
  28. +2
    -0
      src/views/repositoriesView.ts
  29. +34
    -12
      src/views/viewDecorationProvider.ts

+ 24
- 6
package.json View File

@ -4305,6 +4305,24 @@
}
},
{
"id": "gitlens.decorations.statusMergingOrRebasingConflictForegroundColor",
"description": "Specifies the decoration foreground color of the status during a rebase operation with conflicts",
"defaults": {
"light": "#ad0707",
"dark": "#c74e39",
"highContrast": "#c74e39"
}
},
{
"id": "gitlens.decorations.statusMergingOrRebasingForegroundColor",
"description": "Specifies the decoration foreground color of the status during a rebase operation",
"defaults": {
"dark": "#D8AF1B",
"light": "#D8AF1B",
"highContrast": "#D8AF1B"
}
},
{
"id": "gitlens.decorations.workspaceRepoMissingForegroundColor",
"description": "Specifies the decoration foreground color of workspace repos which are missing a local path",
"defaults": {
@ -11673,7 +11691,7 @@
},
{
"command": "gitlens.views.cherryPick",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+current\\b)/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+(current|rebase)\\b)/",
"group": "1_gitlens_actions@1"
},
{
@ -11698,27 +11716,27 @@
},
{
"command": "gitlens.views.resetToCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+rebase\\b)/",
"group": "1_gitlens_actions@4"
},
{
"command": "gitlens.views.resetToTip",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:branch\\b/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:branch\\b(?!.*?\\b\\+rebase\\b)/",
"group": "1_gitlens_actions@4"
},
{
"command": "gitlens.views.resetCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+rebase\\b)/",
"group": "1_gitlens_actions@5"
},
{
"command": "gitlens.views.rebaseOntoCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+rebase\\b)/",
"group": "1_gitlens_actions@6"
},
{
"command": "gitlens.views.switchToCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b/",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:commit\\b(?!.*?\\b\\+rebase\\b)/",
"group": "1_gitlens_actions@7"
},
{

+ 1
- 2
src/commands/quickCommand.steps.ts View File

@ -1891,8 +1891,7 @@ async function getShowCommitOrStashStepItems<
const branch = await Container.instance.git.getBranch(state.repo.path);
const [branches, published] = await Promise.all([
branch != null
? Container.instance.git.getCommitBranches(state.repo.path, state.reference.ref, {
branch: branch.name,
? Container.instance.git.getCommitBranches(state.repo.path, state.reference.ref, branch.name, {
commitDate: isCommit(state.reference) ? state.reference.committer.date : undefined,
})
: undefined,

+ 2
- 0
src/constants.ts View File

@ -72,6 +72,8 @@ export type Colors =
| `${typeof extensionPrefix}.decorations.deletedForegroundColor`
| `${typeof extensionPrefix}.decorations.ignoredForegroundColor`
| `${typeof extensionPrefix}.decorations.modifiedForegroundColor`
| `${typeof extensionPrefix}.decorations.statusMergingOrRebasingConflictForegroundColor`
| `${typeof extensionPrefix}.decorations.statusMergingOrRebasingForegroundColor`
| `${typeof extensionPrefix}.decorations.renamedForegroundColor`
| `${typeof extensionPrefix}.decorations.untrackedForegroundColor`
| `${typeof extensionPrefix}.decorations.workspaceCurrentForegroundColor`

+ 25
- 17
src/env/node/git/git.ts View File

@ -20,6 +20,7 @@ import {
StashPushErrorReason,
WorkspaceUntrustedError,
} from '../../../git/errors';
import type { GitDir } from '../../../git/gitProvider';
import type { GitDiffFilter } from '../../../git/models/diff';
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
import type { GitUser } from '../../../git/models/user';
@ -503,22 +504,29 @@ export class Git {
);
}
branch__containsOrPointsAt(
branchOrTag__containsOrPointsAt(
repoPath: string,
ref: string,
{
mode = 'contains',
name = undefined,
remotes = false,
}: { mode?: 'contains' | 'pointsAt'; name?: string; remotes?: boolean } = {},
options?: {
type?: 'branch' | 'tag';
all?: boolean;
mode?: 'contains' | 'pointsAt';
name?: string;
remotes?: boolean;
},
) {
const params = ['branch'];
if (remotes) {
const params: string[] = [options?.type ?? 'branch'];
if (options?.all) {
params.push('-a');
} else if (options?.remotes) {
params.push('-r');
}
params.push(mode === 'pointsAt' ? `--points-at=${ref}` : `--contains=${ref}`, '--format=%(refname:short)');
if (name != null) {
params.push(name);
params.push(
options?.mode === 'pointsAt' ? `--points-at=${ref}` : `--contains=${ref}`,
'--format=%(refname:short)',
);
if (options?.name != null) {
params.push(options.name);
}
return this.git<string>(
@ -2177,22 +2185,22 @@ export class Git {
}
async readDotGitFile(
repoPath: string,
paths: string[],
gitDir: GitDir,
pathParts: string[],
options?: { numeric?: false; throw?: boolean; trim?: boolean },
): Promise<string | undefined>;
async readDotGitFile(
repoPath: string,
path: string[],
gitDir: GitDir,
pathParts: string[],
options?: { numeric: true; throw?: boolean; trim?: boolean },
): Promise<number | undefined>;
async readDotGitFile(
repoPath: string,
gitDir: GitDir,
pathParts: string[],
options?: { numeric?: boolean; throw?: boolean; trim?: boolean },
): Promise<string | number | undefined> {
try {
const bytes = await workspace.fs.readFile(Uri.file(joinPaths(repoPath, '.git', ...pathParts)));
const bytes = await workspace.fs.readFile(Uri.joinPath(gitDir.uri, ...pathParts));
let contents = textDecoder.decode(bytes);
contents = options?.trim ?? true ? contents.trim() : contents;

+ 134
- 64
src/env/node/git/localGitProvider.ts View File

@ -80,7 +80,7 @@ import type {
import type { GitLog } from '../../../git/models/log';
import type { GitMergeStatus } from '../../../git/models/merge';
import type { GitRebaseStatus } from '../../../git/models/rebase';
import type { GitBranchReference } from '../../../git/models/reference';
import type { GitBranchReference, GitTagReference } from '../../../git/models/reference';
import {
createReference,
getBranchTrackingWithoutRemote,
@ -240,8 +240,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
private readonly _branchesCache = new Map<string, Promise<PagedResult<GitBranch>>>();
private readonly _contributorsCache = new Map<string, Promise<GitContributor[]>>();
private readonly _mergeStatusCache = new Map<string, GitMergeStatus | null>();
private readonly _rebaseStatusCache = new Map<string, GitRebaseStatus | null>();
private readonly _mergeStatusCache = new Map<string, Promise<GitMergeStatus | undefined>>();
private readonly _rebaseStatusCache = new Map<string, Promise<GitRebaseStatus | undefined>>();
private readonly _remotesCache = new Map<string, Promise<GitRemote[]>>();
private readonly _repoInfoCache = new Map<string, RepositoryInfo>();
private readonly _stashesCache = new Map<string, GitStash | null>();
@ -1727,6 +1727,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
};
}
@gate()
@log()
async getBranch(repoPath: string): Promise<GitBranch | undefined> {
let {
@ -1741,16 +1742,17 @@ export class LocalGitProvider implements GitProvider, Disposable {
const [name, upstream] = data[0].split('\n');
if (isDetachedHead(name)) {
const [rebaseStatus, committerDate] = await Promise.all([
const [rebaseStatusResult, committerDateResult] = await Promise.allSettled([
this.getRebaseStatus(repoPath),
this.git.log__recent_committerdate(repoPath, commitOrdering),
]);
const committerDate = getSettledValue(committerDateResult);
branch = new GitBranch(
this.container,
repoPath,
rebaseStatus?.incoming.name ?? name,
getSettledValue(rebaseStatusResult)?.incoming.name ?? name,
false,
true,
committerDate != null ? new Date(Number(committerDate) * 1000) : undefined,
@ -1759,7 +1761,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
undefined,
undefined,
undefined,
rebaseStatus != null,
rebaseStatusResult != null,
);
}
@ -1867,19 +1869,21 @@ export class LocalGitProvider implements GitProvider, Disposable {
async getCommitBranches(
repoPath: string,
ref: string,
options?: { branch?: string; commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
branch?: string | undefined,
options?:
| { all?: boolean; commitDate?: Date; mode?: 'contains' | 'pointsAt' }
| { commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
): Promise<string[]> {
if (options?.branch) {
const data = await this.git.branch__containsOrPointsAt(repoPath, ref, {
if (branch != null) {
const data = await this.git.branchOrTag__containsOrPointsAt(repoPath, ref, {
type: 'branch',
mode: 'contains',
name: options.branch,
name: branch,
});
if (!data) return [];
return [data?.trim()];
return data ? [data?.trim()] : [];
}
const data = await this.git.branch__containsOrPointsAt(repoPath, ref, options);
const data = await this.git.branchOrTag__containsOrPointsAt(repoPath, ref, { type: 'branch', ...options });
if (!data) return [];
return filterMap(data.split('\n'), b => b.trim() || undefined);
@ -2451,6 +2455,18 @@ export class LocalGitProvider implements GitProvider, Disposable {
return getCommitsForGraphCore.call(this, defaultLimit, selectSha);
}
@log()
async getCommitTags(
repoPath: string,
ref: string,
options?: { commitDate?: Date; mode?: 'contains' | 'pointsAt' },
): Promise<string[]> {
const data = await this.git.branchOrTag__containsOrPointsAt(repoPath, ref, { type: 'tag', ...options });
if (!data) return [];
return filterMap(data.split('\n'), b => b.trim() || undefined);
}
getConfig(repoPath: string, key: string): Promise<string | undefined> {
return this.git.config__get(key, repoPath);
}
@ -2898,6 +2914,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
return files[0];
}
@gate()
@debug()
async getGitDir(repoPath: string): Promise<GitDir> {
const repo = this._repoInfoCache.get(repoPath);
@ -3545,20 +3562,25 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
}
@gate()
@log()
async getMergeStatus(repoPath: string): Promise<GitMergeStatus | undefined> {
let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined;
if (status === undefined) {
const merge = await this.git.rev_parse__verify(repoPath, 'MERGE_HEAD');
if (merge != null) {
const [branch, mergeBase, possibleSourceBranches] = await Promise.all([
if (status == null) {
async function getCore(this: LocalGitProvider): Promise<GitMergeStatus | undefined> {
const merge = await this.git.rev_parse__verify(repoPath, 'MERGE_HEAD');
if (merge == null) return undefined;
const [branchResult, mergeBaseResult, possibleSourceBranchesResult] = await Promise.allSettled([
this.getBranch(repoPath),
this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'),
this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }),
this.getCommitBranches(repoPath, 'MERGE_HEAD', undefined, { all: true, mode: 'pointsAt' }),
]);
status = {
const branch = getSettledValue(branchResult);
const mergeBase = getSettledValue(mergeBaseResult);
const possibleSourceBranches = getSettledValue(possibleSourceBranchesResult);
return {
type: 'merge',
repoPath: repoPath,
mergeBase: mergeBase,
@ -3572,66 +3594,110 @@ export class LocalGitProvider implements GitProvider, Disposable {
remote: false,
})
: undefined,
};
} satisfies GitMergeStatus;
}
status = getCore.call(this);
if (this.useCaching) {
this._mergeStatusCache.set(repoPath, status ?? null);
this._mergeStatusCache.set(repoPath, status);
}
}
return status ?? undefined;
return status;
}
@gate()
@log()
async getRebaseStatus(repoPath: string): Promise<GitRebaseStatus | undefined> {
let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined;
if (status === undefined) {
const rebase = await this.git.rev_parse__verify(repoPath, 'REBASE_HEAD');
if (rebase != null) {
let [mergeBase, branch, onto, stepsNumber, stepsMessage, stepsTotal] = await Promise.all([
this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'),
this.git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']),
this.git.readDotGitFile(repoPath, ['rebase-merge', 'onto']),
this.git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }),
if (status == null) {
async function getCore(this: LocalGitProvider): Promise<GitRebaseStatus | undefined> {
const gitDir = await this.getGitDir(repoPath);
const [rebaseMergeHeadResult, rebaseApplyHeadResult] = await Promise.allSettled([
this.git.readDotGitFile(gitDir, ['rebase-merge', 'head-name']),
this.git.readDotGitFile(gitDir, ['rebase-apply', 'head-name']),
]);
const rebaseMergeHead = getSettledValue(rebaseMergeHeadResult);
const rebaseApplyHead = getSettledValue(rebaseApplyHeadResult);
let branch = rebaseApplyHead ?? rebaseMergeHead;
if (branch == null) return undefined;
const path = rebaseApplyHead != null ? 'rebase-apply' : 'rebase-merge';
const [
rebaseHeadResult,
origHeadResult,
ontoResult,
stepsNumberResult,
stepsTotalResult,
stepsMessageResult,
] = await Promise.allSettled([
this.git.rev_parse__verify(repoPath, 'REBASE_HEAD'),
this.git.readDotGitFile(gitDir, [path, 'orig-head']),
this.git.readDotGitFile(gitDir, [path, 'onto']),
this.git.readDotGitFile(gitDir, [path, 'msgnum'], { numeric: true }),
this.git.readDotGitFile(gitDir, [path, 'end'], { numeric: true }),
this.git
.readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true })
.catch(() => this.git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed'])),
this.git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }),
.readDotGitFile(gitDir, [path, 'message'], { throw: true })
.catch(() => this.git.readDotGitFile(gitDir, [path, 'message-squashed'])),
]);
if (branch == null || onto == null) return undefined;
const origHead = getSettledValue(origHeadResult);
const onto = getSettledValue(ontoResult);
if (origHead == null || onto == null) return undefined;
let mergeBase;
const rebaseHead = getSettledValue(rebaseHeadResult);
if (rebaseHead != null) {
mergeBase = await this.getMergeBase(repoPath, rebaseHead, 'HEAD');
} else {
mergeBase = await this.getMergeBase(repoPath, onto, origHead);
}
if (branch.startsWith('refs/heads/')) {
branch = branch.substr(11).trim();
}
const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' });
const [branchTipsResult, tagTipsResult] = await Promise.allSettled([
this.getCommitBranches(repoPath, onto, undefined, { all: true, mode: 'pointsAt' }),
this.getCommitTags(repoPath, onto, { mode: 'pointsAt' }),
]);
const branchTips = getSettledValue(branchTipsResult);
const tagTips = getSettledValue(tagTipsResult);
let possibleSourceBranch: string | undefined;
for (const b of possibleSourceBranches) {
if (b.startsWith('(no branch, rebasing')) continue;
let ontoRef: GitBranchReference | GitTagReference | undefined;
if (branchTips != null) {
for (const ref of branchTips) {
if (ref.startsWith('(no branch, rebasing')) continue;
possibleSourceBranch = b;
break;
ontoRef = createReference(ref, repoPath, {
refType: 'branch',
name: ref,
remote: false,
});
break;
}
}
if (ontoRef == null && tagTips != null) {
for (const ref of tagTips) {
if (ref.startsWith('(no branch, rebasing')) continue;
ontoRef = createReference(ref, repoPath, {
refType: 'tag',
name: ref,
});
break;
}
}
status = {
return {
type: 'rebase',
repoPath: repoPath,
mergeBase: mergeBase,
HEAD: createReference(rebase, repoPath, { refType: 'revision' }),
HEAD: createReference(rebaseHead ?? origHead, repoPath, { refType: 'revision' }),
onto: createReference(onto, repoPath, { refType: 'revision' }),
current:
possibleSourceBranch != null
? createReference(possibleSourceBranch, repoPath, {
refType: 'branch',
name: possibleSourceBranch,
remote: false,
})
: undefined,
current: ontoRef,
incoming: createReference(branch, repoPath, {
refType: 'branch',
name: branch,
@ -3639,23 +3705,27 @@ export class LocalGitProvider implements GitProvider, Disposable {
}),
steps: {
current: {
number: stepsNumber ?? 0,
commit: createReference(rebase, repoPath, {
refType: 'revision',
message: stepsMessage,
}),
number: getSettledValue(stepsNumberResult) ?? 0,
commit:
rebaseHead != null
? createReference(rebaseHead, repoPath, {
refType: 'revision',
message: getSettledValue(stepsMessageResult),
})
: undefined,
},
total: stepsTotal ?? 0,
total: getSettledValue(stepsTotalResult) ?? 0,
},
};
} satisfies GitRebaseStatus;
}
status = getCore.call(this);
if (this.useCaching) {
this._rebaseStatusCache.set(repoPath, status ?? null);
this._rebaseStatusCache.set(repoPath, status);
}
}
return status ?? undefined;
return status;
}
@log()

+ 3
- 3
src/env/node/git/shell.ts View File

@ -1,7 +1,7 @@
import type { ExecException } from 'child_process';
import { exec, execFile } from 'child_process';
import type { Stats } from 'fs';
import { exists, existsSync, statSync } from 'fs';
import { access, constants, existsSync, statSync } from 'fs';
import { join as joinPaths } from 'path';
import * as process from 'process';
import type { CancellationToken } from 'vscode';
@ -290,6 +290,6 @@ export function run(
});
}
export function fsExists(path: string) {
return new Promise<boolean>(resolve => exists(path, exists => resolve(exists)));
export async function fsExists(path: string) {
return new Promise<boolean>(resolve => access(path, constants.F_OK, err => resolve(err == null)));
}

+ 12
- 7
src/git/gitProvider.ts View File

@ -227,13 +227,10 @@ export interface GitProvider extends Disposable {
getCommitBranches(
repoPath: string,
ref: string,
options?: {
branch?: string | undefined;
commitDate?: Date | undefined;
mode?: 'contains' | 'pointsAt' | undefined;
name?: string | undefined;
remotes?: boolean | undefined;
},
branch?: string | undefined,
options?:
| { all?: boolean; commitDate?: Date; mode?: 'contains' | 'pointsAt' }
| { commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
): Promise<string[]>;
getCommitCount(repoPath: string, ref: string): Promise<number | undefined>;
getCommitForFile(
@ -255,6 +252,14 @@ export interface GitProvider extends Disposable {
ref?: string;
},
): Promise<GitGraph>;
getCommitTags(
repoPath: string,
ref: string,
options?: {
commitDate?: Date | undefined;
mode?: 'contains' | 'pointsAt' | undefined;
},
): Promise<string[]>;
getConfig?(repoPath: string, key: string): Promise<string | undefined>;
setConfig?(repoPath: string, key: string, value: string | undefined): Promise<void>;
getContributors(

+ 15
- 2
src/git/gitProviderService.ts View File

@ -1631,10 +1631,13 @@ export class GitProviderService implements Disposable {
getCommitBranches(
repoPath: string | Uri,
ref: string,
options?: { branch?: string; commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
branch?: string | undefined,
options?:
| { all?: boolean; commitDate?: Date; mode?: 'contains' | 'pointsAt' }
| { commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
): Promise<string[]> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitBranches(path, ref, options);
return provider.getCommitBranches(path, ref, branch, options);
}
@log()
@ -1671,6 +1674,16 @@ export class GitProviderService implements Disposable {
}
@log()
getCommitTags(
repoPath: string | Uri,
ref: string,
options?: { commitDate?: Date; mode?: 'contains' | 'pointsAt' },
): Promise<string[]> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitTags(path, ref, options);
}
@log()
async getConfig(repoPath: string | Uri, key: string): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getConfig?.(path, key);

+ 3
- 3
src/git/models/rebase.ts View File

@ -1,4 +1,4 @@
import type { GitBranchReference, GitRevisionReference } from './reference';
import type { GitBranchReference, GitRevisionReference, GitTagReference } from './reference';
export interface GitRebaseStatus {
type: 'rebase';
@ -6,11 +6,11 @@ export interface GitRebaseStatus {
HEAD: GitRevisionReference;
onto: GitRevisionReference;
mergeBase: string | undefined;
current: GitBranchReference | undefined;
current: GitBranchReference | GitTagReference | undefined;
incoming: GitBranchReference;
steps: {
current: { number: number; commit: GitRevisionReference };
current: { number: number; commit: GitRevisionReference | undefined };
total: number;
};
}

+ 76
- 0
src/plus/github/github.ts View File

@ -1591,6 +1591,82 @@ export class GitHubApi implements Disposable {
}
}
@debug<GitHubApi['getCommitTags']>({ args: { 0: '<token>' } })
async getCommitTags(token: string, owner: string, repo: string, ref: string, date: Date): Promise<string[]> {
const scope = getLogScope();
interface QueryResult {
repository: {
refs: {
nodes: {
name: string;
target: {
history: {
nodes: { oid: string }[];
};
};
}[];
};
};
}
try {
const query = `query getCommitTags(
$owner: String!
$repo: String!
$since: GitTimestamp!
$until: GitTimestamp!
) {
repository(owner: $owner, name: $repo) {
refs(first: 20, refPrefix: "refs/tags/") {
nodes {
name
target {
... on Commit {
history(first: 3, since: $since until: $until) {
nodes { oid }
}
}
}
}
}
}
}`;
const rsp = await this.graphql<QueryResult>(
undefined,
token,
query,
{
owner: owner,
repo: repo,
since: date.toISOString(),
until: date.toISOString(),
},
scope,
);
const nodes = rsp?.repository?.refs?.nodes;
if (nodes == null) return [];
const tags = [];
for (const tag of nodes) {
for (const commit of tag.target.history.nodes) {
if (commit.oid === ref) {
tags.push(tag.name);
break;
}
}
}
return tags;
} catch (ex) {
if (ex instanceof ProviderRequestNotFoundError) return [];
throw this.handleException(ex, undefined, scope);
}
}
@debug<GitHubApi['getNextCommitRefs']>({ args: { 0: '<token>' } })
async getNextCommitRefs(
token: string,

+ 35
- 3
src/plus/github/githubGitProvider.ts View File

@ -1048,7 +1048,10 @@ export class GitHubGitProvider implements GitProvider, Disposable {
async getCommitBranches(
repoPath: string,
ref: string,
options?: { branch?: string; commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
branch?: string | undefined,
options?:
| { all?: boolean; commitDate?: Date; mode?: 'contains' | 'pointsAt' }
| { commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
): Promise<string[]> {
if (repoPath == null || options?.commitDate == null) return [];
@ -1059,12 +1062,12 @@ export class GitHubGitProvider implements GitProvider, Disposable {
let branches;
if (options?.branch) {
if (branch) {
branches = await github.getCommitOnBranch(
session.accessToken,
metadata.repo.owner,
metadata.repo.name,
options?.branch,
branch,
ref,
options?.commitDate,
);
@ -1592,6 +1595,35 @@ export class GitHubGitProvider implements GitProvider, Disposable {
}
@log()
async getCommitTags(
repoPath: string,
ref: string,
options?: { commitDate?: Date; mode?: 'contains' | 'pointsAt' },
): Promise<string[]> {
if (repoPath == null || options?.commitDate == null) return [];
const scope = getLogScope();
try {
const { metadata, github, session } = await this.ensureRepositoryContext(repoPath);
const tags = await github.getCommitTags(
session.accessToken,
metadata.repo.owner,
metadata.repo.name,
ref,
options?.commitDate,
);
return tags;
} catch (ex) {
Logger.error(ex, scope);
debugger;
return [];
}
}
@log()
async getContributors(
repoPath: string,
_options?: { all?: boolean; ref?: string; stats?: boolean },

+ 20
- 20
src/system/promise.ts View File

@ -4,29 +4,29 @@ export type PromiseOrValue = Promise | T;
export function any<T>(...promises: Promise<T>[]): Promise<T> {
return new Promise<T>((resolve, reject) => {
const errors: Error[] = [];
let settled = false;
const onFullfilled = (r: T) => {
settled = true;
resolve(r);
};
let errors: Error[];
const onRejected = (ex: Error) => {
if (settled) return;
if (errors == null) {
errors = [ex];
} else {
errors.push(ex);
}
if (promises.length - errors.length < 1) {
reject(new AggregateError(errors));
}
};
for (const promise of promises) {
// eslint-disable-next-line no-loop-func
void (async () => {
try {
const result = await promise;
if (settled) return;
resolve(result);
settled = true;
} catch (ex) {
errors.push(ex);
} finally {
if (!settled) {
if (promises.length - errors.length < 1) {
reject(new AggregateError(errors));
settled = true;
}
}
}
})();
promise.then(onFullfilled, onRejected);
}
});
}

+ 1
- 0
src/views/branchesView.ts View File

@ -213,6 +213,7 @@ export class BranchesView extends ViewBase<'branches', BranchesViewNode, Branche
const branches = await this.container.git.getCommitBranches(
commit.repoPath,
commit.ref,
undefined,
isCommit(commit) ? { commitDate: commit.committer.date } : undefined,
);
if (branches.length === 0) return undefined;

+ 1
- 2
src/views/commitsView.ts View File

@ -355,8 +355,7 @@ export class CommitsView extends ViewBase<'commits', CommitsViewNode, CommitsVie
if (branch == null) return undefined;
// Check if the commit exists on the current branch
const branches = await this.container.git.getCommitBranches(commit.repoPath, commit.ref, {
branch: branch.name,
const branches = await this.container.git.getCommitBranches(commit.repoPath, commit.ref, branch.name, {
commitDate: isCommit(commit) ? commit.committer.date : undefined,
});
if (!branches.length) return undefined;

+ 1
- 2
src/views/nodes/branchNode.ts View File

@ -17,7 +17,6 @@ import { map } from '../../system/iterable';
import type { Deferred } from '../../system/promise';
import { defer, getSettledValue } from '../../system/promise';
import { pad } from '../../system/string';
import { RemotesView } from '../remotesView';
import type { ViewsWithBranches } from '../viewBase';
import { BranchTrackingStatusNode } from './branchTrackingStatusNode';
import { CommitNode } from './commitNode';
@ -216,7 +215,7 @@ export class BranchNode extends ViewRefNode
const children = [];
if (this.options.showComparison !== false && !(this.view instanceof RemotesView)) {
if (this.options.showComparison !== false && this.view.type !== 'remotes') {
children.push(
new CompareBranchNode(
this.uri,

+ 1
- 2
src/views/nodes/branchesNode.ts View File

@ -4,7 +4,6 @@ import type { Repository } from '../../git/models/repository';
import { makeHierarchical } from '../../system/array';
import { gate } from '../../system/decorators/gate';
import { debug } from '../../system/decorators/log';
import { RepositoriesView } from '../repositoriesView';
import type { ViewsWithBranchesNode } from '../viewBase';
import { BranchNode } from './branchNode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
@ -55,7 +54,7 @@ export class BranchesNode extends ViewNode {
false,
{
showComparison:
this.view instanceof RepositoriesView
this.view.type === 'repositories'
? this.view.config.branches.showBranchComparison
: this.view.config.showBranchComparison,
},

+ 9
- 6
src/views/nodes/commitNode.ts View File

@ -20,7 +20,6 @@ import type { Deferred } from '../../system/promise';
import { defer, getSettledValue } from '../../system/promise';
import { sortCompare } from '../../system/string';
import type { FileHistoryView } from '../fileHistoryView';
import { TagsView } from '../tagsView';
import type { ViewsWithCommits } from '../viewBase';
import { CommitFileNode } from './commitFileNode';
import type { FileNode } from './folderNode';
@ -39,10 +38,10 @@ export class CommitNode extends ViewRefNode
view: ViewsWithCommits | FileHistoryView,
parent: ViewNode,
public readonly commit: GitCommit,
private readonly unpublished?: boolean,
protected readonly unpublished?: boolean,
public readonly branch?: GitBranch,
private readonly getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined,
private readonly _options: { expand?: boolean } = {},
protected readonly getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined,
protected readonly _options: { expand?: boolean } = {},
) {
super(commit.getGitUri(), view, parent);
@ -77,7 +76,7 @@ export class CommitNode extends ViewRefNode
let pullRequest;
if (
!(this.view instanceof TagsView) &&
this.view.type !== 'tags' &&
!this.unpublished &&
getContext('gitlens:hasConnectedRemotes') &&
this.view.config.pullRequests.enabled &&
@ -257,7 +256,7 @@ export class CommitNode extends ViewRefNode
pr = getSettledValue(prResult);
}
const tooltip = await CommitFormatter.fromTemplateAsync(this.view.config.formats.commits.tooltip, this.commit, {
const tooltip = await CommitFormatter.fromTemplateAsync(this.getTooltipTemplate(), this.commit, {
enrichedAutolinks: enrichedAutolinks,
dateFormat: configuration.get('defaultDateFormat'),
getBranchAndTagTips: this.getBranchAndTagTips,
@ -275,4 +274,8 @@ export class CommitNode extends ViewRefNode
return markdown;
}
protected getTooltipTemplate(): string {
return this.view.config.formats.commits.tooltip;
}
}

+ 54
- 38
src/views/nodes/fileRevisionAsCommitNode.ts View File

@ -3,6 +3,7 @@ import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleSta
import type { DiffWithPreviousCommandArgs } from '../../commands';
import type { Colors } from '../../constants';
import { Commands } from '../../constants';
import type { Container } from '../../container';
import { CommitFormatter } from '../../git/formatters/commitFormatter';
import { StatusFileFormatter } from '../../git/formatters/statusFormatter';
import { GitUri } from '../../git/gitUri';
@ -60,10 +61,8 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
]);
const mergeStatus = getSettledValue(mergeStatusResult);
if (mergeStatus == null) return [];
const rebaseStatus = getSettledValue(rebaseStatusResult);
if (rebaseStatus == null) return [];
if (mergeStatus == null && rebaseStatus == null) return [];
return [
new MergeConflictCurrentChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file),
@ -204,43 +203,13 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
private async getTooltip() {
const [remotesResult, _] = await Promise.allSettled([
this.view.container.git.getBestRemotesWithProviders(this.commit.repoPath),
this.commit.message == null ? this.commit.ensureFullDetails() : undefined,
]);
const remotes = getSettledValue(remotesResult, []);
const [remote] = remotes;
let enrichedAutolinks;
let pr;
if (remote?.hasRichIntegration()) {
const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
pauseOnCancelOrTimeoutMapTuplePromise(this.commit.getEnrichedAutolinks(remote)),
this.commit.getAssociatedPullRequest(remote),
]);
enrichedAutolinks = getSettledValue(enrichedAutolinksResult)?.value;
pr = getSettledValue(prResult);
}
const status = StatusFileFormatter.fromTemplate(
`\${status}\${ (originalPath)}\${'&nbsp;&nbsp;•&nbsp;&nbsp;'changesDetail}`,
this.file,
);
const tooltip = await CommitFormatter.fromTemplateAsync(
this.view.config.formats.commits.tooltipWithStatus.replace('{{slot-status}}', status),
const tooltip = await getFileRevisionAsCommitTooltip(
this.view.container,
this.commit,
this.file,
this.view.config.formats.commits.tooltipWithStatus,
{
enrichedAutolinks: enrichedAutolinks,
dateFormat: configuration.get('defaultDateFormat'),
getBranchAndTagTips: this._options.getBranchAndTagTips,
messageAutolinks: true,
messageIndent: 4,
pullRequest: pr,
outputFormat: 'markdown',
remotes: remotes,
unpublished: this._options.unpublished,
},
);
@ -248,7 +217,54 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
const markdown = new MarkdownString(tooltip, true);
markdown.supportHtml = true;
markdown.isTrusted = true;
return markdown;
}
}
export async function getFileRevisionAsCommitTooltip(
container: Container,
commit: GitCommit,
file: GitFile,
tooltipWithStatusFormat: string,
options?: {
getBranchAndTagTips?: (sha: string, options?: { compact?: boolean }) => string | undefined;
unpublished?: boolean;
},
) {
const [remotesResult, _] = await Promise.allSettled([
container.git.getBestRemotesWithProviders(commit.repoPath),
commit.message == null ? commit.ensureFullDetails() : undefined,
]);
const remotes = getSettledValue(remotesResult, []);
const [remote] = remotes;
let enrichedAutolinks;
let pr;
if (remote?.hasRichIntegration()) {
const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
pauseOnCancelOrTimeoutMapTuplePromise(commit.getEnrichedAutolinks(remote)),
commit.getAssociatedPullRequest(remote),
]);
enrichedAutolinks = getSettledValue(enrichedAutolinksResult)?.value;
pr = getSettledValue(prResult);
}
const status = StatusFileFormatter.fromTemplate(
`\${status}\${ (originalPath)}\${'&nbsp;&nbsp;•&nbsp;&nbsp;'changesDetail}`,
file,
);
return CommitFormatter.fromTemplateAsync(tooltipWithStatusFormat.replace('{{slot-status}}', status), commit, {
enrichedAutolinks: enrichedAutolinks,
dateFormat: configuration.get('defaultDateFormat'),
getBranchAndTagTips: options?.getBranchAndTagTips,
messageAutolinks: true,
messageIndent: 4,
pullRequest: pr,
outputFormat: 'markdown',
remotes: remotes,
unpublished: options?.unpublished,
});
}

+ 43
- 27
src/views/nodes/mergeConflictCurrentChangesNode.ts View File

@ -2,8 +2,8 @@ import type { Command } from 'vscode';
import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import type { DiffWithCommandArgs } from '../../commands';
import { Commands, GlyphChars } from '../../constants';
import { CommitFormatter } from '../../git/formatters/commitFormatter';
import { GitUri } from '../../git/gitUri';
import type { GitCommit } from '../../git/models/commit';
import type { GitFile } from '../../git/models/file';
import type { GitMergeStatus } from '../../git/models/merge';
import type { GitRebaseStatus } from '../../git/models/rebase';
@ -13,6 +13,7 @@ import { configuration } from '../../system/configuration';
import type { FileHistoryView } from '../fileHistoryView';
import type { LineHistoryView } from '../lineHistoryView';
import type { ViewsWithCommits } from '../viewBase';
import { getFileRevisionAsCommitTooltip } from './fileRevisionAsCommitNode';
import { ContextValues, ViewNode } from './viewNode';
export class MergeConflictCurrentChangesNode extends ViewNode<ViewsWithCommits | FileHistoryView | LineHistoryView> {
@ -25,12 +26,20 @@ export class MergeConflictCurrentChangesNode extends ViewNode
super(GitUri.fromFile(file, status.repoPath, 'HEAD'), view, parent);
}
private _commit: Promise<GitCommit | undefined> | undefined;
private async getCommit(): Promise<GitCommit | undefined> {
if (this._commit == null) {
this._commit = this.view.container.git.getCommit(this.status.repoPath, 'HEAD');
}
return this._commit;
}
getChildren(): ViewNode[] {
return [];
}
async getTreeItem(): Promise<TreeItem> {
const commit = await this.view.container.git.getCommit(this.status.repoPath, 'HEAD');
const commit = await this.getCommit();
const item = new TreeItem('Current changes', TreeItemCollapsibleState.None);
item.contextValue = ContextValues.MergeConflictCurrentChanges;
@ -41,31 +50,6 @@ export class MergeConflictCurrentChangesNode extends ViewNode
? (await commit?.getAvatarUri({ defaultStyle: configuration.get('defaultGravatarsStyle') })) ??
new ThemeIcon('diff')
: new ThemeIcon('diff');
const markdown = new MarkdownString(
`Current changes to $(file)${GlyphChars.Space}${this.file.path} on ${getReferenceLabel(
this.status.current,
)}${
commit != null
? `\n\n${await CommitFormatter.fromTemplateAsync(
`\${avatar}&nbsp;__\${author}__, \${ago} &nbsp; _(\${date})_ \n\n\${message}\n\n\${link}\${' via 'pullRequest}`,
commit,
{
avatarSize: 16,
dateFormat: configuration.get('defaultDateFormat'),
// messageAutolinks: true,
messageIndent: 4,
outputFormat: 'markdown',
},
)}`
: ''
}`,
true,
);
markdown.supportHtml = true;
markdown.isTrusted = true;
item.tooltip = markdown;
item.command = this.getCommand();
return item;
@ -102,4 +86,36 @@ export class MergeConflictCurrentChangesNode extends ViewNode
},
});
}
override async resolveTreeItem(item: TreeItem): Promise<TreeItem> {
if (item.tooltip == null) {
item.tooltip = await this.getTooltip();
}
return item;
}
private async getTooltip() {
const commit = await this.getCommit();
const markdown = new MarkdownString(
`Current changes on ${getReferenceLabel(this.status.current, { label: false })}\\\n$(file)${
GlyphChars.Space
}${this.file.path}`,
true,
);
if (commit == null) return markdown;
const tooltip = await getFileRevisionAsCommitTooltip(
this.view.container,
commit,
this.file,
this.view.config.formats.commits.tooltipWithStatus,
);
markdown.appendMarkdown(`\n\n${tooltip}`);
markdown.isTrusted = true;
return markdown;
}
}

+ 55
- 0
src/views/nodes/mergeConflictFilesNode.ts View File

@ -0,0 +1,55 @@
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GitUri } from '../../git/gitUri';
import type { GitMergeStatus } from '../../git/models/merge';
import type { GitRebaseStatus } from '../../git/models/rebase';
import type { GitStatusFile } from '../../git/models/status';
import { makeHierarchical } from '../../system/array';
import { joinPaths, normalizePath } from '../../system/path';
import { pluralize, sortCompare } from '../../system/string';
import type { ViewsWithCommits } from '../viewBase';
import type { FileNode } from './folderNode';
import { FolderNode } from './folderNode';
import { MergeConflictFileNode } from './mergeConflictFileNode';
import { ViewNode } from './viewNode';
export class MergeConflictFilesNode extends ViewNode<ViewsWithCommits> {
constructor(
view: ViewsWithCommits,
protected override readonly parent: ViewNode,
private readonly status: GitMergeStatus | GitRebaseStatus,
private readonly conflicts: GitStatusFile[],
) {
super(GitUri.fromRepoPath(status.repoPath), view, parent);
}
get repoPath(): string {
return this.uri.repoPath!;
}
getChildren(): ViewNode[] {
let children: (FileNode | FolderNode)[] = this.conflicts.map(
f => new MergeConflictFileNode(this.view, this, f, this.status),
);
if (this.view.config.files.layout !== 'list') {
const hierarchy = makeHierarchical(
children as FileNode[],
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, hierarchy, this.repoPath, '', undefined);
children = root.getChildren();
} else {
children.sort((a, b) => sortCompare(a.label!, b.label!));
}
return children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(pluralize('conflict', this.conflicts.length), TreeItemCollapsibleState.Expanded);
return item;
}
}

+ 59
- 40
src/views/nodes/mergeConflictIncomingChangesNode.ts View File

@ -2,8 +2,8 @@ import type { Command } from 'vscode';
import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import type { DiffWithCommandArgs } from '../../commands';
import { Commands, GlyphChars } from '../../constants';
import { CommitFormatter } from '../../git/formatters/commitFormatter';
import { GitUri } from '../../git/gitUri';
import type { GitCommit } from '../../git/models/commit';
import type { GitFile } from '../../git/models/file';
import type { GitMergeStatus } from '../../git/models/merge';
import type { GitRebaseStatus } from '../../git/models/rebase';
@ -13,6 +13,7 @@ import { configuration } from '../../system/configuration';
import type { FileHistoryView } from '../fileHistoryView';
import type { LineHistoryView } from '../lineHistoryView';
import type { ViewsWithCommits } from '../viewBase';
import { getFileRevisionAsCommitTooltip } from './fileRevisionAsCommitNode';
import { ContextValues, ViewNode } from './viewNode';
export class MergeConflictIncomingChangesNode extends ViewNode<ViewsWithCommits | FileHistoryView | LineHistoryView> {
@ -25,15 +26,23 @@ export class MergeConflictIncomingChangesNode extends ViewNode
super(GitUri.fromFile(file, status.repoPath, status.HEAD.ref), view, parent);
}
private _commit: Promise<GitCommit | undefined> | undefined;
private async getCommit(): Promise<GitCommit | undefined> {
if (this._commit == null) {
const ref = this.status.type === 'rebase' ? this.status.steps.current.commit?.ref : this.status.HEAD.ref;
if (ref == null) return undefined;
this._commit = this.view.container.git.getCommit(this.status.repoPath, ref);
}
return this._commit;
}
getChildren(): ViewNode[] {
return [];
}
async getTreeItem(): Promise<TreeItem> {
const commit = await this.view.container.git.getCommit(
this.status.repoPath,
this.status.type === 'rebase' ? this.status.steps.current.commit.ref : this.status.HEAD.ref,
);
const commit = await this.getCommit();
const item = new TreeItem('Incoming changes', TreeItemCollapsibleState.None);
item.contextValue = ContextValues.MergeConflictIncomingChanges;
@ -49,41 +58,6 @@ export class MergeConflictIncomingChangesNode extends ViewNode
? (await commit?.getAvatarUri({ defaultStyle: configuration.get('defaultGravatarsStyle') })) ??
new ThemeIcon('diff')
: new ThemeIcon('diff');
const markdown = new MarkdownString(
`Incoming changes to $(file)${GlyphChars.Space}${this.file.path}${
this.status.incoming != null
? ` from ${getReferenceLabel(this.status.incoming)}${
commit != null
? `\n\n${await CommitFormatter.fromTemplateAsync(
`\${avatar}&nbsp;__\${author}__, \${ago} &nbsp; _(\${date})_ \n\n\${message}\n\n\${link}\${' via 'pullRequest}`,
commit,
{
avatarSize: 16,
dateFormat: configuration.get('defaultDateFormat'),
// messageAutolinks: true,
messageIndent: 4,
outputFormat: 'markdown',
},
)}`
: this.status.type === 'rebase'
? `\n\n${getReferenceLabel(this.status.steps.current.commit, {
capitalize: true,
label: false,
})}`
: `\n\n${getReferenceLabel(this.status.HEAD, {
capitalize: true,
label: false,
})}`
}`
: ''
}`,
true,
);
markdown.supportHtml = true;
markdown.isTrusted = true;
item.tooltip = markdown;
item.command = this.getCommand();
return item;
@ -121,4 +95,49 @@ export class MergeConflictIncomingChangesNode extends ViewNode
},
});
}
override async resolveTreeItem(item: TreeItem): Promise<TreeItem> {
if (item.tooltip == null) {
item.tooltip = await this.getTooltip();
}
return item;
}
private async getTooltip() {
const commit = await this.getCommit();
const markdown = new MarkdownString(
`Incoming changes from ${getReferenceLabel(this.status.incoming, { label: false })}\\\n$(file)${
GlyphChars.Space
}${this.file.path}`,
true,
);
if (commit == null) {
markdown.appendMarkdown(
this.status.type === 'rebase'
? `\n\n${getReferenceLabel(this.status.steps.current.commit, {
capitalize: true,
label: false,
})}`
: `\n\n${getReferenceLabel(this.status.HEAD, {
capitalize: true,
label: false,
})}`,
);
return markdown;
}
const tooltip = await getFileRevisionAsCommitTooltip(
this.view.container,
commit,
this.file,
this.view.config.formats.commits.tooltipWithStatus,
);
markdown.appendMarkdown(`\n\n${tooltip}`);
markdown.isTrusted = true;
return markdown;
}
}

+ 27
- 38
src/views/nodes/mergeStatusNode.ts View File

@ -1,17 +1,13 @@
import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import type { CoreColors } from '../../constants';
import type { Colors } from '../../constants';
import { GitUri } from '../../git/gitUri';
import type { GitBranch } from '../../git/models/branch';
import type { GitMergeStatus } from '../../git/models/merge';
import { getReferenceLabel } from '../../git/models/reference';
import type { GitStatus } from '../../git/models/status';
import { makeHierarchical } from '../../system/array';
import { joinPaths, normalizePath } from '../../system/path';
import { pluralize, sortCompare } from '../../system/string';
import { pluralize } from '../../system/string';
import type { ViewsWithCommits } from '../viewBase';
import type { FileNode } from './folderNode';
import { FolderNode } from './folderNode';
import { MergeConflictFileNode } from './mergeConflictFileNode';
import { MergeConflictFilesNode } from './mergeConflictFilesNode';
import { ContextValues, getViewNodeId, ViewNode } from './viewNode';
export class MergeStatusNode extends ViewNode<ViewsWithCommits> {
@ -26,7 +22,7 @@ export class MergeStatusNode extends ViewNode {
) {
super(GitUri.fromRepoPath(mergeStatus.repoPath), view, parent);
this.updateContext({ branch: branch, root: root });
this.updateContext({ branch: branch, root: root, status: 'merging' });
this._uniqueId = getViewNodeId('merge-status', this.context);
}
@ -35,50 +31,43 @@ export class MergeStatusNode extends ViewNode {
}
getChildren(): ViewNode[] {
if (this.status?.hasConflicts !== true) return [];
let children: FileNode[] = this.status.conflicts.map(
f => new MergeConflictFileNode(this.view, this, f, this.mergeStatus),
);
if (this.view.config.files.layout !== 'list') {
const hierarchy = makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, hierarchy, this.repoPath, '', undefined);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) => sortCompare(a.label!, b.label!));
}
return children;
return this.status?.hasConflicts
? [new MergeConflictFilesNode(this.view, this, this.mergeStatus, this.status.conflicts)]
: [];
}
getTreeItem(): TreeItem {
const hasConflicts = this.status?.hasConflicts === true;
const item = new TreeItem(
`${this.status?.hasConflicts ? 'Resolve conflicts before merging' : 'Merging'} ${
`${hasConflicts ? 'Resolve conflicts before merging' : 'Merging'} ${
this.mergeStatus.incoming != null
? `${getReferenceLabel(this.mergeStatus.incoming, { expand: false, icon: false })} `
: ''
}into ${getReferenceLabel(this.mergeStatus.current, { expand: false, icon: false })}`,
TreeItemCollapsibleState.Expanded,
hasConflicts ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
);
item.id = this.id;
item.contextValue = ContextValues.Merge;
item.description = this.status?.hasConflicts ? pluralize('conflict', this.status.conflicts.length) : undefined;
item.iconPath = this.status?.hasConflicts
? new ThemeIcon('warning', new ThemeColor('list.warningForeground' satisfies CoreColors))
: new ThemeIcon('debug-pause', new ThemeColor('list.foreground' satisfies CoreColors));
item.description = hasConflicts ? pluralize('conflict', this.status.conflicts.length) : undefined;
item.iconPath = hasConflicts
? new ThemeIcon(
'warning',
new ThemeColor(
'gitlens.decorations.statusMergingOrRebasingConflictForegroundColor' satisfies Colors,
),
)
: new ThemeIcon(
'warning',
new ThemeColor('gitlens.decorations.statusMergingOrRebasingForegroundColor' satisfies Colors),
);
const markdown = new MarkdownString(
`${`Merging ${
this.mergeStatus.incoming != null ? getReferenceLabel(this.mergeStatus.incoming) : ''
}into ${getReferenceLabel(this.mergeStatus.current)}`}${
this.status?.hasConflicts ? `\n\n${pluralize('conflicted file', this.status.conflicts.length)}` : ''
this.mergeStatus.incoming != null ? getReferenceLabel(this.mergeStatus.incoming, { label: false }) : ''
}into ${getReferenceLabel(this.mergeStatus.current, { label: false })}`}${
hasConflicts
? `\n\nResolve ${pluralize('conflict', this.status.conflicts.length)} before continuing`
: ''
}`,
true,
);

+ 26
- 0
src/views/nodes/rebaseCommitNode.ts View File

@ -0,0 +1,26 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitFormatter } from '../../git/formatters/commitFormatter';
import { CommitNode } from './commitNode';
import { ContextValues } from './viewNode';
export class RebaseCommitNode extends CommitNode {
// eslint-disable-next-line @typescript-eslint/require-await
override async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(
`Paused at commit ${this.commit.shortSha}`,
this._options.expand ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed,
);
item.id = this.id;
item.contextValue = `${ContextValues.Commit}+rebase`;
item.description = CommitFormatter.fromTemplate(`\${message}`, this.commit, {
messageTruncateAtNewLine: true,
});
item.iconPath = new ThemeIcon('debug-pause');
return item;
}
protected override getTooltipTemplate(): string {
return `Rebase paused at ${super.getTooltipTemplate()}`;
}
}

+ 57
- 176
src/views/nodes/rebaseStatusNode.ts View File

@ -1,29 +1,16 @@
import type { Command } from 'vscode';
import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import type { DiffWithPreviousCommandArgs } from '../../commands';
import type { CoreColors } from '../../constants';
import { Commands } from '../../constants';
import { CommitFormatter } from '../../git/formatters/commitFormatter';
import type { Colors } from '../../constants';
import { GitUri } from '../../git/gitUri';
import type { GitBranch } from '../../git/models/branch';
import type { GitCommit } from '../../git/models/commit';
import type { GitRebaseStatus } from '../../git/models/rebase';
import type { GitRevisionReference } from '../../git/models/reference';
import { getReferenceLabel } from '../../git/models/reference';
import type { GitStatus } from '../../git/models/status';
import { makeHierarchical } from '../../system/array';
import { pauseOnCancelOrTimeoutMapTuplePromise } from '../../system/cancellation';
import { executeCoreCommand } from '../../system/command';
import { configuration } from '../../system/configuration';
import { joinPaths, normalizePath } from '../../system/path';
import { getSettledValue } from '../../system/promise';
import { pluralize, sortCompare } from '../../system/string';
import { pluralize } from '../../system/string';
import type { ViewsWithCommits } from '../viewBase';
import { CommitFileNode } from './commitFileNode';
import type { FileNode } from './folderNode';
import { FolderNode } from './folderNode';
import { MergeConflictFileNode } from './mergeConflictFileNode';
import { ContextValues, getViewNodeId, ViewNode, ViewRefNode } from './viewNode';
import { MergeConflictFilesNode } from './mergeConflictFilesNode';
import { RebaseCommitNode } from './rebaseCommitNode';
import { ContextValues, getViewNodeId, ViewNode } from './viewNode';
export class RebaseStatusNode extends ViewNode<ViewsWithCommits> {
constructor(
@ -37,7 +24,7 @@ export class RebaseStatusNode extends ViewNode {
) {
super(GitUri.fromRepoPath(rebaseStatus.repoPath), view, parent);
this.updateContext({ branch: branch, root: root });
this.updateContext({ branch: branch, root: root, status: 'rebasing' });
this._uniqueId = getViewNodeId('merge-status', this.context);
}
@ -46,65 +33,80 @@ export class RebaseStatusNode extends ViewNode {
}
async getChildren(): Promise<ViewNode[]> {
let children: FileNode[] =
this.status?.conflicts.map(f => new MergeConflictFileNode(this.view, this, f, this.rebaseStatus)) ?? [];
if (this.view.config.files.layout !== 'list') {
const hierarchy = makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, hierarchy, this.repoPath, '', undefined);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) => sortCompare(a.label!, b.label!));
const children: (MergeConflictFilesNode | RebaseCommitNode)[] = [];
const revision = this.rebaseStatus.steps.current.commit;
if (revision != null) {
const commit =
revision != null
? await this.view.container.git.getCommit(this.rebaseStatus.repoPath, revision.ref)
: undefined;
if (commit != null) {
children.push(new RebaseCommitNode(this.view, this, commit));
}
}
const commit = await this.view.container.git.getCommit(
this.rebaseStatus.repoPath,
this.rebaseStatus.steps.current.commit.ref,
);
if (commit != null) {
children.unshift(new RebaseCommitNode(this.view, this, commit) as any);
if (this.status?.hasConflicts) {
children.push(new MergeConflictFilesNode(this.view, this, this.rebaseStatus, this.status.conflicts));
}
return children;
}
getTreeItem(): TreeItem {
const started = this.rebaseStatus.steps.total > 0;
const pausedAtCommit = started && this.rebaseStatus.steps.current.commit != null;
const hasConflicts = this.status?.hasConflicts === true;
const item = new TreeItem(
`${this.status?.hasConflicts ? 'Resolve conflicts to continue rebasing' : 'Rebasing'} ${
`${hasConflicts ? 'Resolve conflicts to continue rebasing' : started ? 'Rebasing' : 'Pending rebase of'} ${
this.rebaseStatus.incoming != null
? `${getReferenceLabel(this.rebaseStatus.incoming, { expand: false, icon: false })}`
: ''
} (${this.rebaseStatus.steps.current.number}/${this.rebaseStatus.steps.total})`,
TreeItemCollapsibleState.Expanded,
} onto ${getReferenceLabel(this.rebaseStatus.current ?? this.rebaseStatus.onto, {
expand: false,
icon: false,
})}${started ? ` (${this.rebaseStatus.steps.current.number}/${this.rebaseStatus.steps.total})` : ''}`,
pausedAtCommit ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None,
);
item.id = this.id;
item.contextValue = ContextValues.Rebase;
item.description = this.status?.hasConflicts ? pluralize('conflict', this.status.conflicts.length) : undefined;
item.iconPath = this.status?.hasConflicts
? new ThemeIcon('warning', new ThemeColor('list.warningForeground' satisfies CoreColors))
: new ThemeIcon('debug-pause', new ThemeColor('list.foreground' satisfies CoreColors));
item.description = hasConflicts ? pluralize('conflict', this.status.conflicts.length) : undefined;
item.iconPath = hasConflicts
? new ThemeIcon(
'warning',
new ThemeColor(
'gitlens.decorations.statusMergingOrRebasingConflictForegroundColor' satisfies Colors,
),
)
: new ThemeIcon(
'warning',
new ThemeColor('gitlens.decorations.statusMergingOrRebasingForegroundColor' satisfies Colors),
);
const markdown = new MarkdownString(
`${`Rebasing ${
this.rebaseStatus.incoming != null ? getReferenceLabel(this.rebaseStatus.incoming) : ''
}onto ${getReferenceLabel(this.rebaseStatus.current)}`}\n\nStep ${
this.rebaseStatus.steps.current.number
} of ${this.rebaseStatus.steps.total}\\\nPaused at ${getReferenceLabel(
this.rebaseStatus.steps.current.commit,
{ icon: true },
)}${this.status?.hasConflicts ? `\n\n${pluralize('conflicted file', this.status.conflicts.length)}` : ''}`,
`${`${started ? 'Rebasing' : 'Pending rebase of'} ${
this.rebaseStatus.incoming != null
? getReferenceLabel(this.rebaseStatus.incoming, { label: false })
: ''
} onto ${getReferenceLabel(this.rebaseStatus.current ?? this.rebaseStatus.onto, { label: false })}`}${
started
? `\n\nPaused at step ${this.rebaseStatus.steps.current.number} of ${
this.rebaseStatus.steps.total
}${
hasConflicts
? `\\\nResolve ${pluralize('conflict', this.status.conflicts.length)} before continuing`
: ''
}`
: ''
}`,
true,
);
markdown.supportHtml = true;
markdown.isTrusted = true;
item.tooltip = markdown;
item.resourceUri = Uri.parse(`gitlens-view://status/rebasing${hasConflicts ? '/conflicts' : ''}`);
return item;
}
@ -116,124 +118,3 @@ export class RebaseStatusNode extends ViewNode {
});
}
}
export class RebaseCommitNode extends ViewRefNode<ViewsWithCommits, GitRevisionReference> {
constructor(
view: ViewsWithCommits,
parent: ViewNode,
public readonly commit: GitCommit,
) {
super(commit.getGitUri(), view, parent);
}
override toClipboard(): string {
return `${this.commit.shortSha}: ${this.commit.summary}`;
}
get ref(): GitRevisionReference {
return this.commit;
}
async getChildren(): Promise<ViewNode[]> {
const commit = this.commit;
const commits = await commit.getCommitsForFiles();
let children: FileNode[] = commits.map(c => new CommitFileNode(this.view, this, c.file!, c));
if (this.view.config.files.layout !== 'list') {
const hierarchy = makeHierarchical(
children,
n => n.uri.relativePath.split('/'),
(...parts: string[]) => normalizePath(joinPaths(...parts)),
this.view.config.files.compact,
);
const root = new FolderNode(this.view, this, hierarchy, this.repoPath, '', undefined);
children = root.getChildren() as FileNode[];
} else {
children.sort((a, b) => sortCompare(a.label!, b.label!));
}
return children;
}
getTreeItem(): TreeItem {
const item = new TreeItem(`Paused at commit ${this.commit.shortSha}`, TreeItemCollapsibleState.Collapsed);
// item.contextValue = ContextValues.RebaseCommit;
item.description = CommitFormatter.fromTemplate(`\${message}`, this.commit, {
messageTruncateAtNewLine: true,
});
item.iconPath = new ThemeIcon('git-commit');
return item;
}
override getCommand(): Command | undefined {
const commandArgs: DiffWithPreviousCommandArgs = {
commit: this.commit,
uri: this.uri,
line: 0,
showOptions: {
preserveFocus: true,
preview: true,
},
};
return {
title: 'Open Changes with Previous Revision',
command: Commands.DiffWithPrevious,
arguments: [undefined, commandArgs],
};
}
override async resolveTreeItem(item: TreeItem): Promise<TreeItem> {
if (item.tooltip == null) {
item.tooltip = await this.getTooltip();
}
return item;
}
private async getTooltip() {
const [remotesResult, _] = await Promise.allSettled([
this.view.container.git.getBestRemotesWithProviders(this.commit.repoPath),
this.commit.message == null ? this.commit.ensureFullDetails() : undefined,
]);
const remotes = getSettledValue(remotesResult, []);
const [remote] = remotes;
let enrichedAutolinks;
let pr;
if (remote?.hasRichIntegration()) {
const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
pauseOnCancelOrTimeoutMapTuplePromise(this.commit.getEnrichedAutolinks(remote)),
this.commit.getAssociatedPullRequest(remote),
]);
enrichedAutolinks = getSettledValue(enrichedAutolinksResult)?.value;
pr = getSettledValue(prResult);
}
const tooltip = await CommitFormatter.fromTemplateAsync(
`Rebase paused at ${this.view.config.formats.commits.tooltip}`,
this.commit,
{
enrichedAutolinks: enrichedAutolinks,
dateFormat: configuration.get('defaultDateFormat'),
messageAutolinks: true,
messageIndent: 4,
pullRequest: pr,
outputFormat: 'markdown',
remotes: remotes,
},
);
const markdown = new MarkdownString(tooltip, true);
markdown.supportHtml = true;
markdown.isTrusted = true;
return markdown;
}
}

+ 2
- 6
src/views/nodes/statusFilesNode.ts View File

@ -10,7 +10,6 @@ import { filter, flatMap, map } from '../../system/iterable';
import { joinPaths, normalizePath } from '../../system/path';
import { pluralize, sortCompare } from '../../system/string';
import type { ViewsWithWorkingTree } from '../viewBase';
import { WorktreesView } from '../worktreesView';
import type { FileNode } from './folderNode';
import { FolderNode } from './folderNode';
import { StatusFileNode } from './statusFileNode';
@ -68,10 +67,7 @@ export class StatusFilesNode extends ViewNode {
}
}
if (
(this.view instanceof WorktreesView || this.view.config.includeWorkingTree) &&
this.status.files.length !== 0
) {
if ((this.view.type === 'worktrees' || this.view.config.includeWorkingTree) && this.status.files.length !== 0) {
files.unshift(
...flatMap(this.status.files, f =>
map(f.getPseudoCommits(this.view.container, undefined), c => this.getFileWithPseudoCommit(f, c)),
@ -113,7 +109,7 @@ export class StatusFilesNode extends ViewNode {
async getTreeItem(): Promise<TreeItem> {
let files =
this.view instanceof WorktreesView || this.view.config.includeWorkingTree ? this.status.files.length : 0;
this.view.type === 'worktrees' || this.view.config.includeWorkingTree ? this.status.files.length : 0;
if (this.range != null) {
if (this.status.upstream != null && this.status.state.ahead > 0) {

+ 6
- 2
src/views/nodes/viewNode.ts View File

@ -111,6 +111,7 @@ export interface AmbientContext {
readonly repository?: Repository;
readonly root?: boolean;
readonly searchId?: string;
readonly status?: 'merging' | 'rebasing';
readonly storedComparisonId?: string;
readonly tag?: GitTag;
readonly workspace?: CloudWorkspace | LocalWorkspace;
@ -145,10 +146,13 @@ export function getViewNodeId(type: string, context: AmbientContext): string {
uniqueness += `/branch/${context.branch.id}`;
}
if (context.branchStatus != null) {
uniqueness += `/status/${context.branchStatus.upstream ?? '-'}`;
uniqueness += `/branch-status/${context.branchStatus.upstream ?? '-'}`;
}
if (context.branchStatusUpstreamType != null) {
uniqueness += `/status-direction/${context.branchStatusUpstreamType}`;
uniqueness += `/branch-status-direction/${context.branchStatusUpstreamType}`;
}
if (context.status != null) {
uniqueness += `/status/${context.status}`;
}
if (context.reflog != null) {
uniqueness += `/reflog/${context.reflog.sha}+${context.reflog.selector}+${context.reflog.command}+${

+ 1
- 0
src/views/remotesView.ts View File

@ -209,6 +209,7 @@ export class RemotesView extends ViewBase<'remotes', RemotesViewNode, RemotesVie
const branches = await this.container.git.getCommitBranches(
commit.repoPath,
commit.ref,
undefined,
isCommit(commit) ? { commitDate: commit.committer.date, remotes: true } : { remotes: true },
);
if (branches.length === 0) return undefined;

+ 2
- 0
src/views/repositoriesView.ts View File

@ -329,6 +329,7 @@ export class RepositoriesView extends ViewBase<'repositories', RepositoriesNode,
let branches = await this.container.git.getCommitBranches(
commit.repoPath,
commit.ref,
undefined,
isCommit(commit) ? { commitDate: commit.committer.date } : undefined,
);
if (branches.length !== 0) {
@ -362,6 +363,7 @@ export class RepositoriesView extends ViewBase<'repositories', RepositoriesNode,
branches = await this.container.git.getCommitBranches(
commit.repoPath,
commit.ref,
undefined,
isCommit(commit) ? { commitDate: commit.committer.date, remotes: true } : { remotes: true },
);
if (branches.length === 0) return undefined;

+ 34
- 12
src/views/viewDecorationProvider.ts View File

@ -19,19 +19,18 @@ export class ViewFileDecorationProvider implements FileDecorationProvider, Dispo
provideFileDecoration: (uri, token) => {
if (uri.scheme !== 'gitlens-view') return undefined;
if (uri.authority === 'branch') {
return this.provideBranchCurrentDecoration(uri, token);
switch (uri.authority) {
case 'branch':
return this.provideBranchCurrentDecoration(uri, token);
case 'remote':
return this.provideRemoteDefaultDecoration(uri, token);
case 'status':
return this.provideStatusDecoration(uri, token);
case 'workspaces':
return this.provideWorkspaceDecoration(uri, token);
default:
return undefined;
}
if (uri.authority === 'remote') {
return this.provideRemoteDefaultDecoration(uri, token);
}
if (uri.authority === 'workspaces') {
return this.provideWorkspaceDecoration(uri, token);
}
return undefined;
},
}),
window.registerFileDecorationProvider(this),
@ -233,4 +232,27 @@ export class ViewFileDecorationProvider implements FileDecorationProvider, Dispo
tooltip: 'Default Remote',
};
}
provideStatusDecoration(uri: Uri, _token: CancellationToken): FileDecoration | undefined {
const [, status, conflicts] = uri.path.split('/');
switch (status) {
case 'rebasing':
if (conflicts) {
return {
badge: '!',
color: new ThemeColor(
'gitlens.decorations.statusMergingOrRebasingConflictForegroundColor' satisfies Colors,
),
};
}
return {
color: new ThemeColor(
'gitlens.decorations.statusMergingOrRebasingForegroundColor' satisfies Colors,
),
};
default:
return undefined;
}
}
}

Loading…
Cancel
Save