Browse Source

Adds new repository commands (fetch, pull, push) (wip)

Closes #470 - Adds fetch command to Repository explorer
main
Eric Amodio 6 years ago
parent
commit
a722a995ea
13 changed files with 308 additions and 18 deletions
  1. +5
    -0
      images/dark/icon-pull.svg
  2. +4
    -0
      images/dark/icon-push.svg
  3. +5
    -0
      images/light/icon-pull.svg
  4. +4
    -0
      images/light/icon-push.svg
  5. +77
    -0
      package.json
  6. +4
    -3
      src/system/date.ts
  7. +17
    -0
      src/system/function.ts
  8. +22
    -0
      src/views/explorerCommands.ts
  9. +9
    -0
      src/views/gitExplorer.ts
  10. +68
    -6
      src/views/nodes/common.ts
  11. +1
    -1
      src/views/nodes/explorerNode.ts
  12. +10
    -0
      src/views/nodes/repositoriesNode.ts
  13. +82
    -8
      src/views/nodes/repositoryNode.ts

+ 5
- 0
images/dark/icon-pull.svg View File

@ -0,0 +1,5 @@
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="M3.947 11.812h8.105L8 18.83l-4.053-7.018z"/>
<path fill="#C5C5C5" d="M8.974 7.82v7.79H7.026V7.82h1.948z"/>
<path fill="#C5C5C5" d="M14.438 3.17v3.22H1.562V3.17h12.876z"/>
</svg>

+ 4
- 0
images/dark/icon-push.svg View File

@ -0,0 +1,4 @@
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#C5C5C5" d="M12.066 14.735H3.934L8 7.695l4.066 7.04z"/>
<path fill="#C5C5C5" d="M7.022 18.758v-7.827h1.956v7.827H7.022zM14.438 3.17v3.22H1.562V3.17h12.876z"/>
</svg>

+ 5
- 0
images/light/icon-pull.svg View File

@ -0,0 +1,5 @@
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="M3.947 11.812h8.105L8 18.83l-4.053-7.018z"/>
<path fill="#424242" d="M8.974 7.82v7.79H7.026V7.82h1.948z"/>
<path fill="#424242" d="M14.438 3.17v3.22H1.562V3.17h12.876z"/>
</svg>

+ 4
- 0
images/light/icon-push.svg View File

@ -0,0 +1,4 @@
<svg width="16" height="22" xmlns="http://www.w3.org/2000/svg">
<path fill="#424242" d="M12.066 14.735H3.934L8 7.695l4.066 7.04z"/>
<path fill="#424242" d="M7.022 18.758v-7.827h1.956v7.827H7.022zM14.438 3.17v3.22H1.562V3.17h12.876z"/>
</svg>

+ 77
- 0
package.json View File

@ -1829,6 +1829,42 @@
"category": "GitLens"
},
{
"command": "gitlens.gitExplorer.fetchAll",
"title": "Fetch Repositories",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-sync.svg",
"light": "images/light/icon-sync.svg"
}
},
{
"command": "gitlens.explorers.fetch",
"title": "Fetch Repository",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-sync.svg",
"light": "images/light/icon-sync.svg"
}
},
{
"command": "gitlens.explorers.pull",
"title": "Pull Repository",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-pull.svg",
"light": "images/light/icon-pull.svg"
}
},
{
"command": "gitlens.explorers.push",
"title": "Push Repository",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-push.svg",
"light": "images/light/icon-push.svg"
}
},
{
"command": "gitlens.explorers.openDirectoryDiff",
"title": "Open Directory Compare",
"category": "GitLens"
@ -2385,6 +2421,22 @@
"when": "false"
},
{
"command": "gitlens.gitExplorer.fetchAll",
"when": "false"
},
{
"command": "gitlens.explorers.fetch",
"when": "false"
},
{
"command": "gitlens.explorers.pull",
"when": "false"
},
{
"command": "gitlens.explorers.push",
"when": "false"
},
{
"command": "gitlens.explorers.openChanges",
"when": "false"
},
@ -2749,6 +2801,11 @@
"group": "2_files@100"
},
{
"command": "gitlens.copyRemoteFileUrlToClipboard",
"when": "gitlens:enabled && gitlens:activeFileStatus =~ /remotes/ && config.gitlens.menus.editorTab.remote",
"group": "2_files@101"
},
{
"command": "gitlens.diffWithPrevious",
"when": "gitlens:enabled && config.gitlens.menus.editorTab.compare",
"group": "1_gitlens_1@1"
@ -2857,6 +2914,11 @@
"group": "navigation@1"
},
{
"command": "gitlens.gitExplorer.fetchAll",
"when": "view =~ /^gitlens.gitExplorer:/",
"group": "navigation@7"
},
{
"command": "gitlens.gitExplorer.refresh",
"when": "view =~ /^gitlens.gitExplorer:/",
"group": "navigation@8"
@ -3204,6 +3266,21 @@
"group": "7_gitlens_more@1"
},
{
"command": "gitlens.explorers.fetch",
"when": "viewItem == gitlens:repository && gitlens:hasRemotes",
"group": "inline@97"
},
{
"command": "gitlens.explorers.push",
"when": "viewItem == gitlens:repository && gitlens:hasRemotes",
"group": "inline@99"
},
{
"command": "gitlens.explorers.pull",
"when": "viewItem == gitlens:repository && gitlens:hasRemotes",
"group": "inline@98"
},
{
"command": "gitlens.openRepoInRemote",
"when": "viewItem == gitlens:repository && gitlens:hasRemotes",
"group": "1_gitlens@1"

+ 4
- 3
src/system/date.ts View File

@ -2,9 +2,6 @@
import { distanceInWordsToNow as _fromNow, format as _format } from 'date-fns';
import * as en from 'date-fns/locale/en';
const MillisecondsPerMinute = 60000; // 60 * 1000
const MillisecondsPerDay = 86400000; // 24 * 60 * 60 * 1000
// Taken from https://github.com/date-fns/date-fns/blob/601bc8e5708cbaebee5389bdaf51c2b4b33b73c4/src/locale/en/build_distance_in_words_locale/index.js
function buildDistanceInWordsLocale() {
const distanceInWordsLocale: { [key: string]: string | { one: string; other: string } } = {
@ -118,6 +115,10 @@ patch.distanceInWords = buildDistanceInWordsLocale();
const formatterOptions = { addSuffix: true, locale: patch };
export namespace Dates {
export const MillisecondsPerMinute = 60000; // 60 * 1000
export const MillisecondsPerHour = 3600000; // 60 * 60 * 1000
export const MillisecondsPerDay = 86400000; // 24 * 60 * 60 * 1000
export interface IDateFormatter {
fromNow(): string;
format(format: string): string;

+ 17
- 0
src/system/function.ts View File

@ -1,3 +1,5 @@
import { Disposable } from 'vscode';
'use strict';
const _debounce = require('lodash.debounce');
const _once = require('lodash.once');
@ -83,6 +85,21 @@ export namespace Functions {
};
}
export function interval(fn: (...args: any[]) => void, ms: number): Disposable {
let timer: NodeJS.Timer | undefined;
const disposable = {
dispose: () => {
if (timer !== undefined) {
clearInterval(timer);
timer = undefined;
}
}
};
timer = setInterval(fn, ms);
return disposable;
}
export async function wait(ms: number) {
await new Promise(resolve => setTimeout(resolve, ms));
}

+ 22
- 0
src/views/explorerCommands.ts View File

@ -49,6 +49,10 @@ export class ExplorerCommands implements Disposable {
private _terminalCwd: string | undefined;
constructor() {
commands.registerCommand('gitlens.explorers.fetch', this.fetch, this);
commands.registerCommand('gitlens.explorers.pull', this.pull, this);
commands.registerCommand('gitlens.explorers.push', this.push, this);
commands.registerCommand('gitlens.explorers.exploreRepoRevision', this.exploreRepoRevision, this);
commands.registerCommand('gitlens.explorers.openChanges', this.openChanges, this);
@ -102,6 +106,24 @@ export class ExplorerCommands implements Disposable {
this._disposable && this._disposable.dispose();
}
private fetch(node: RepositoryNode) {
if (!(node instanceof RepositoryNode)) return;
return node.fetch();
}
private pull(node: RepositoryNode) {
if (!(node instanceof RepositoryNode)) return;
return node.pull();
}
private push(node: RepositoryNode) {
if (!(node instanceof RepositoryNode)) return;
return node.push();
}
private async applyChanges(node: CommitFileNode | StashFileNode | StatusFileNode) {
await this.openFile(node);

+ 9
- 0
src/views/gitExplorer.ts View File

@ -24,6 +24,9 @@ export class GitExplorer extends ExplorerBase {
protected registerCommands() {
Container.explorerCommands;
commands.registerCommand(this.getQualifiedCommand('fetchAll'), () => this.fetchAll(), this);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this);
commands.registerCommand(
this.getQualifiedCommand('refreshNode'),
@ -102,6 +105,12 @@ export class GitExplorer extends ExplorerBase {
return { ...Container.config.explorers, ...Container.config.gitExplorer };
}
private fetchAll() {
if (this._root === undefined) return;
return this._root.fetchAll();
}
private async setAutoRefresh(enabled: boolean, workspaceEnabled?: boolean) {
if (enabled) {
if (workspaceEnabled === undefined) {

+ 68
- 6
src/views/nodes/common.ts View File

@ -7,9 +7,9 @@ import { ExplorerNode, ResourceType, unknownGitUri } from '../nodes/explorerNode
export class MessageNode extends ExplorerNode {
constructor(
private readonly message: string,
private readonly tooltip?: string,
private readonly iconPath?:
private readonly _message: string,
private readonly _tooltip?: string,
private readonly _iconPath?:
| string
| Uri
| {
@ -26,14 +26,76 @@ export class MessageNode extends ExplorerNode {
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this.message, TreeItemCollapsibleState.None);
const item = new TreeItem(this._message, TreeItemCollapsibleState.None);
item.contextValue = ResourceType.Message;
item.tooltip = this.tooltip;
item.iconPath = this.iconPath;
item.tooltip = this._tooltip;
item.iconPath = this._iconPath;
return item;
}
}
export class UpdateableMessageNode extends ExplorerNode {
constructor(
public readonly id: string,
private _message: string,
private _tooltip?: string,
private _iconPath?:
| string
| Uri
| {
light: string | Uri;
dark: string | Uri;
}
| ThemeIcon
) {
super(unknownGitUri);
}
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> {
return [];
}
getTreeItem(): TreeItem | Promise<TreeItem> {
const item = new TreeItem(this._message, TreeItemCollapsibleState.None);
item.id = this.id;
item.contextValue = ResourceType.Message;
item.tooltip = this._tooltip;
item.iconPath = this._iconPath;
return item;
}
update(
changes: {
message?: string;
tooltip?: string | null;
iconPath?:
| string
| null
| Uri
| {
light: string | Uri;
dark: string | Uri;
}
| ThemeIcon;
},
explorer: Explorer
) {
if (changes.message !== undefined) {
this._message = changes.message;
}
if (changes.tooltip !== undefined) {
this._tooltip = changes.tooltip === null ? undefined : changes.tooltip;
}
if (changes.iconPath !== undefined) {
this._iconPath = changes.iconPath === null ? undefined : changes.iconPath;
}
explorer.triggerNodeUpdate(this);
}
}
export abstract class PagerNode extends ExplorerNode {
protected _args: RefreshNodeCommandArgs = {};

+ 1
- 1
src/views/nodes/explorerNode.ts View File

@ -99,7 +99,7 @@ export abstract class SubscribeableExplorerNode exte
constructor(
uri: GitUri,
protected readonly explorer: TExplorer
public readonly explorer: TExplorer
) {
super(uri);

+ 10
- 0
src/views/nodes/repositoriesNode.ts View File

@ -56,6 +56,16 @@ export class RepositoriesNode extends SubscribeableExplorerNode {
return item;
}
async fetchAll() {
if (this._children === undefined || this._children.length === 0) return;
for (const node of this._children) {
if (node instanceof MessageNode) continue;
await node.fetch();
}
}
async refresh() {
if (this._children === undefined) return;

+ 82
- 8
src/views/nodes/repositoryNode.ts View File

@ -1,5 +1,7 @@
'use strict';
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { commands, Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import {
@ -12,7 +14,7 @@ import {
RepositoryFileSystemChangeEvent
} from '../../git/gitService';
import { Logger } from '../../logger';
import { Strings } from '../../system';
import { Dates, Functions, Strings } from '../../system';
import { GitExplorer } from '../gitExplorer';
import { BranchesNode } from './branchesNode';
import { BranchNode } from './branchNode';
@ -26,6 +28,7 @@ import { TagsNode } from './tagsNode';
export class RepositoryNode extends SubscribeableExplorerNode<GitExplorer> {
private _children: ExplorerNode[] | undefined;
private _lastFetched: number = 0;
private _status: Promise<GitStatus | undefined>;
constructor(
@ -90,7 +93,16 @@ export class RepositoryNode extends SubscribeableExplorerNode {
async getTreeItem(): Promise<TreeItem> {
let label = this.repo.formattedName || this.uri.repoPath || '';
let tooltip = this.repo.formattedName ? `${this.repo.formattedName}\n${this.uri.repoPath}` : this.uri.repoPath;
this._lastFetched = await this.getLastFetched();
const lastFetchedTooltip = this.formatLastFetched({
prefix: `${Strings.pad(GlyphChars.Dash, 2, 2)}Last fetched on `,
format: 'dddd MMMM Do, YYYY h:mm a'
});
let tooltip = this.repo.formattedName
? `${this.repo.formattedName}${lastFetchedTooltip}\n${this.uri.repoPath}`
: `${this.uri.repoPath}${lastFetchedTooltip}`;
let iconSuffix = '';
let workingStatus = '';
@ -109,7 +121,7 @@ export class RepositoryNode extends SubscribeableExplorerNode {
prefix: `${GlyphChars.Space} `
});
label += ` ${Strings.pad(GlyphChars.Dash, 2, 3)}${status.branch}${upstreamStatus}${workingStatus}`;
label += `${Strings.pad(GlyphChars.Dash, 3, 3)}${status.branch}${upstreamStatus}${workingStatus}`;
iconSuffix = workingStatus ? '-blue' : '';
if (status.upstream !== undefined) {
@ -137,7 +149,12 @@ export class RepositoryNode extends SubscribeableExplorerNode {
}
}
const item = new TreeItem(label, TreeItemCollapsibleState.Expanded);
const item = new TreeItem(
`${label}${this.formatLastFetched({
prefix: `${Strings.pad(GlyphChars.Dash, 4, 4)}Last fetched `
})}`,
TreeItemCollapsibleState.Expanded
);
item.id = this.id;
item.contextValue = ResourceType.Repository;
item.tooltip = tooltip;
@ -151,6 +168,26 @@ export class RepositoryNode extends SubscribeableExplorerNode {
return item;
}
async fetch() {
await commands.executeCommand('git.fetch', this.repo.path);
await this.updateLastFetched();
this.explorer.triggerNodeUpdate(this);
}
async pull() {
await commands.executeCommand('git.pull', this.repo.path);
await this.updateLastFetched();
this.explorer.triggerNodeUpdate(this);
}
async push() {
await commands.executeCommand('git.push', this.repo.path);
this.explorer.triggerNodeUpdate(this);
}
refresh() {
this._status = this.repo.getStatus();
@ -162,9 +199,13 @@ export class RepositoryNode extends SubscribeableExplorerNode {
const disposables = [this.repo.onDidChange(this.onRepoChanged, this)];
if (this.includeWorkingTree) {
disposables.push(this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), {
dispose: () => this.repo.stopWatchingFileSystem()
});
disposables.push(
this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this),
{
dispose: () => this.repo.stopWatchingFileSystem()
},
Functions.interval(() => void this.updateLastFetched(), 60000)
);
this.repo.startWatchingFileSystem();
}
@ -220,4 +261,37 @@ export class RepositoryNode extends SubscribeableExplorerNode {
}
}
}
private formatLastFetched(options: { prefix?: string; format?: string } = {}) {
if (this._lastFetched === 0) return '';
if (options.format === undefined && Date.now() - this._lastFetched < Dates.MillisecondsPerDay) {
return `${options.prefix || ''}${Dates.toFormatter(new Date(this._lastFetched)).fromNow()}`;
}
return `${options.prefix || ''}${Dates.toFormatter(new Date(this._lastFetched)).format(
options.format || 'MMM DD, YYYY'
)}`;
}
private async getLastFetched(): Promise<number> {
const hasRemotes = await this.repo.hasRemotes();
if (!hasRemotes) return 0;
return new Promise<number>((resolve, reject) =>
fs.stat(path.join(this.repo.path, '.git/FETCH_HEAD'), (err, stat) =>
resolve(err ? 0 : stat.mtime.getTime())
)
);
}
private async updateLastFetched() {
const prevLastFetched = this._lastFetched;
this._lastFetched = await this.getLastFetched();
// If the fetched date hasn't changed and it was over a day ago, kick out
if (this._lastFetched === prevLastFetched && Date.now() - this._lastFetched >= Dates.MillisecondsPerDay) return;
this.explorer.triggerNodeUpdate(this);
}
}

Loading…
Cancel
Save