OXYGENHUB
# f6bda77ecb9f oxygen-vscode / src / operations.ts
3.2 KiB · last changed Milestone 0.2: discard, commit selected paths, commit chosen hunks, amend, undo, Oxygen Operations view with undo to here 1 month ago source blame history raw
1
// The operation log as a tree view (spec §5.9): newest first, with
2
// "Undo to here" on any entry, snapshots hidden unless configured.
3
4
import * as vscode from "vscode";
5
import { OperationInfo } from "./oxy";
6
import { Repository } from "./repository";
7
8
export class OperationItem extends vscode.TreeItem {
9
  constructor(
10
    readonly repository: Repository,
11
    readonly op: OperationInfo,
12
    readonly stepsBack: number,
13
  ) {
14
    super(op.command, vscode.TreeItemCollapsibleState.None);
15
    this.id = `${repository.root}:${op.id}`;
16
    this.description = timeAgo(op.when_ms);
17
    this.tooltip = `${op.command}\n${new Date(op.when_ms).toLocaleString()}\n${op.id.slice(0, 12)}`;
18
    this.iconPath = new vscode.ThemeIcon(op.current ? "circle-filled" : op.snapshot ? "circle-small" : "circle-outline");
19
    this.contextValue = op.current ? "current" : "operation";
20
  }
21
}
22
23
export class OperationsProvider implements vscode.TreeDataProvider<OperationItem | RepositoryItem> {
24
  private readonly emitter = new vscode.EventEmitter<void>();
25
  readonly onDidChangeTreeData = this.emitter.event;
26
27
  constructor(private readonly repositories: () => Repository[]) {}
28
29
  refresh(): void {
30
    this.emitter.fire();
31
  }
32
33
  getTreeItem(element: OperationItem | RepositoryItem): vscode.TreeItem {
34
    return element;
35
  }
36
37
  async getChildren(element?: OperationItem | RepositoryItem): Promise<(OperationItem | RepositoryItem)[]> {
38
    const repos = this.repositories();
39
    if (!element) {
40
      if (repos.length === 1) {
41
        return this.operations(repos[0]);
42
      }
43
      return repos.map((r) => new RepositoryItem(r));
44
    }
45
    if (element instanceof RepositoryItem) {
46
      return this.operations(element.repository);
47
    }
48
    return [];
49
  }
50
51
  private async operations(repo: Repository): Promise<OperationItem[]> {
52
    const showSnapshots = vscode.workspace.getConfiguration("oxygen").get<boolean>("showSnapshotOperations", false);
53
    try {
54
      const ops = await repo.oxy.opLog(repo.root);
55
      const items: OperationItem[] = [];
56
      // Steps back count every operation, snapshots included, since undo
57
      // skips snapshots itself only when they are the newest entries.
58
      let steps = 0;
59
      for (const op of ops) {
60
        if (showSnapshots || !op.snapshot || op.current) {
61
          items.push(new OperationItem(repo, op, steps));
62
        }
63
        if (!op.snapshot) {
64
          steps += 1;
65
        }
66
      }
67
      return items.slice(0, 200);
68
    } catch {
69
      return [];
70
    }
71
  }
72
}
73
74
export class RepositoryItem extends vscode.TreeItem {
75
  constructor(readonly repository: Repository) {
76
    super(repository.root.split("/").pop() ?? repository.root, vscode.TreeItemCollapsibleState.Expanded);
77
    this.iconPath = new vscode.ThemeIcon("repo");
78
    this.contextValue = "repository";
79
  }
80
}
81
82
export function timeAgo(ms: number): string {
83
  const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
84
  if (s < 45) return "just now";
85
  if (s < 90) return "a minute ago";
86
  if (s < 2700) return `${Math.round(s / 60)} minutes ago`;
87
  if (s < 5400) return "an hour ago";
88
  if (s < 79200) return `${Math.round(s / 3600)} hours ago`;
89
  if (s < 129600) return "yesterday";
90
  if (s < 2592000) return `${Math.round(s / 86400)} days ago`;
91
  return new Date(ms).toLocaleDateString();
92
}