You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

365 lines
9.6 KiB

4 years ago
  1. import {
  2. CancellationToken,
  3. commands,
  4. ConfigurationChangeEvent,
  5. Disposable,
  6. ProgressLocation,
  7. TreeItem,
  8. TreeItemCollapsibleState,
  9. window,
  10. } from 'vscode';
  11. import {
  12. BranchesViewConfig,
  13. configuration,
  14. ViewBranchesLayout,
  15. ViewFilesLayout,
  16. ViewShowBranchComparison,
  17. } from '../configuration';
  18. import { Commands } from '../constants';
  19. import { Container } from '../container';
  20. import { GitUri } from '../git/gitUri';
  21. import {
  22. GitBranchReference,
  23. GitCommit,
  24. GitReference,
  25. GitRevisionReference,
  26. RepositoryChange,
  27. RepositoryChangeComparisonMode,
  28. RepositoryChangeEvent,
  29. } from '../git/models';
  30. import { executeCommand } from '../system/command';
  31. import { gate } from '../system/decorators/gate';
  32. import {
  33. BranchesNode,
  34. BranchNode,
  35. BranchOrTagFolderNode,
  36. RepositoriesSubscribeableNode,
  37. RepositoryFolderNode,
  38. RepositoryNode,
  39. ViewNode,
  40. } from './nodes';
  41. import { ViewBase } from './viewBase';
  42. export class BranchesRepositoryNode extends RepositoryFolderNode<BranchesView, BranchesNode> {
  43. async getChildren(): Promise<ViewNode[]> {
  44. if (this.child == null) {
  45. this.child = new BranchesNode(this.uri, this.view, this, this.repo);
  46. }
  47. return this.child.getChildren();
  48. }
  49. protected changed(e: RepositoryChangeEvent) {
  50. return e.changed(
  51. RepositoryChange.Config,
  52. RepositoryChange.Heads,
  53. RepositoryChange.Index,
  54. RepositoryChange.Remotes,
  55. RepositoryChange.RemoteProviders,
  56. RepositoryChange.Status,
  57. RepositoryChange.Unknown,
  58. RepositoryChangeComparisonMode.Any,
  59. );
  60. }
  61. }
  62. export class BranchesViewNode extends RepositoriesSubscribeableNode<BranchesView, BranchesRepositoryNode> {
  63. async getChildren(): Promise<ViewNode[]> {
  64. if (this.children == null) {
  65. const repositories = this.view.container.git.openRepositories;
  66. if (repositories.length === 0) {
  67. this.view.message = 'No branches could be found.';
  68. return [];
  69. }
  70. this.view.message = undefined;
  71. const splat = repositories.length === 1;
  72. this.children = repositories.map(
  73. r => new BranchesRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, splat),
  74. );
  75. }
  76. if (this.children.length === 1) {
  77. const [child] = this.children;
  78. const branches = await child.repo.getBranches({ filter: b => !b.remote });
  79. if (branches.values.length === 0) {
  80. this.view.message = 'No branches could be found.';
  81. this.view.title = 'Branches';
  82. void child.ensureSubscription();
  83. return [];
  84. }
  85. this.view.message = undefined;
  86. this.view.title = `Branches (${branches.values.length})`;
  87. return child.getChildren();
  88. }
  89. return this.children;
  90. }
  91. getTreeItem(): TreeItem {
  92. const item = new TreeItem('Branches', TreeItemCollapsibleState.Expanded);
  93. return item;
  94. }
  95. }
  96. export class BranchesView extends ViewBase<BranchesViewNode, BranchesViewConfig> {
  97. protected readonly configKey = 'branches';
  98. constructor(container: Container) {
  99. super('gitlens.views.branches', 'Branches', container);
  100. }
  101. override get canReveal(): boolean {
  102. return this.config.reveal || !configuration.get('views.repositories.showBranches');
  103. }
  104. protected getRoot() {
  105. return new BranchesViewNode(this);
  106. }
  107. protected registerCommands(): Disposable[] {
  108. void this.container.viewCommands;
  109. return [
  110. commands.registerCommand(
  111. this.getQualifiedCommand('copy'),
  112. () => executeCommand(Commands.ViewsCopy, this.selection),
  113. this,
  114. ),
  115. commands.registerCommand(
  116. this.getQualifiedCommand('refresh'),
  117. () => {
  118. this.container.git.resetCaches('branches');
  119. return this.refresh(true);
  120. },
  121. this,
  122. ),
  123. commands.registerCommand(
  124. this.getQualifiedCommand('setLayoutToList'),
  125. () => this.setLayout(ViewBranchesLayout.List),
  126. this,
  127. ),
  128. commands.registerCommand(
  129. this.getQualifiedCommand('setLayoutToTree'),
  130. () => this.setLayout(ViewBranchesLayout.Tree),
  131. this,
  132. ),
  133. commands.registerCommand(
  134. this.getQualifiedCommand('setFilesLayoutToAuto'),
  135. () => this.setFilesLayout(ViewFilesLayout.Auto),
  136. this,
  137. ),
  138. commands.registerCommand(
  139. this.getQualifiedCommand('setFilesLayoutToList'),
  140. () => this.setFilesLayout(ViewFilesLayout.List),
  141. this,
  142. ),
  143. commands.registerCommand(
  144. this.getQualifiedCommand('setFilesLayoutToTree'),
  145. () => this.setFilesLayout(ViewFilesLayout.Tree),
  146. this,
  147. ),
  148. commands.registerCommand(
  149. this.getQualifiedCommand('setShowAvatarsOn'),
  150. () => this.setShowAvatars(true),
  151. this,
  152. ),
  153. commands.registerCommand(
  154. this.getQualifiedCommand('setShowAvatarsOff'),
  155. () => this.setShowAvatars(false),
  156. this,
  157. ),
  158. commands.registerCommand(
  159. this.getQualifiedCommand('setShowBranchComparisonOn'),
  160. () => this.setShowBranchComparison(true),
  161. this,
  162. ),
  163. commands.registerCommand(
  164. this.getQualifiedCommand('setShowBranchComparisonOff'),
  165. () => this.setShowBranchComparison(false),
  166. this,
  167. ),
  168. commands.registerCommand(
  169. this.getQualifiedCommand('setShowBranchPullRequestOn'),
  170. () => this.setShowBranchPullRequest(true),
  171. this,
  172. ),
  173. commands.registerCommand(
  174. this.getQualifiedCommand('setShowBranchPullRequestOff'),
  175. () => this.setShowBranchPullRequest(false),
  176. this,
  177. ),
  178. ];
  179. }
  180. protected override filterConfigurationChanged(e: ConfigurationChangeEvent) {
  181. const changed = super.filterConfigurationChanged(e);
  182. if (
  183. !changed &&
  184. !configuration.changed(e, 'defaultDateFormat') &&
  185. !configuration.changed(e, 'defaultDateShortFormat') &&
  186. !configuration.changed(e, 'defaultDateSource') &&
  187. !configuration.changed(e, 'defaultDateStyle') &&
  188. !configuration.changed(e, 'defaultGravatarsStyle') &&
  189. !configuration.changed(e, 'defaultTimeFormat') &&
  190. !configuration.changed(e, 'sortBranchesBy')
  191. ) {
  192. return false;
  193. }
  194. return true;
  195. }
  196. findBranch(branch: GitBranchReference, token?: CancellationToken) {
  197. if (branch.remote) return undefined;
  198. const repoNodeId = RepositoryNode.getId(branch.repoPath);
  199. return this.findNode((n: any) => n.branch?.ref === branch.ref, {
  200. allowPaging: true,
  201. maxDepth: 4,
  202. canTraverse: n => {
  203. if (n instanceof BranchesViewNode) return true;
  204. if (n instanceof BranchesRepositoryNode || n instanceof BranchOrTagFolderNode) {
  205. return n.id.startsWith(repoNodeId);
  206. }
  207. return false;
  208. },
  209. token: token,
  210. });
  211. }
  212. async findCommit(commit: GitCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
  213. const repoNodeId = RepositoryNode.getId(commit.repoPath);
  214. // Get all the branches the commit is on
  215. const branches = await this.container.git.getCommitBranches(
  216. commit.repoPath,
  217. commit.ref,
  218. GitCommit.is(commit) ? { commitDate: commit.committer.date } : undefined,
  219. );
  220. if (branches.length === 0) return undefined;
  221. return this.findNode((n: any) => n.commit?.ref === commit.ref, {
  222. allowPaging: true,
  223. maxDepth: 5,
  224. canTraverse: async n => {
  225. if (n instanceof BranchesViewNode) return true;
  226. if (n instanceof BranchesRepositoryNode || n instanceof BranchOrTagFolderNode) {
  227. return n.id.startsWith(repoNodeId);
  228. }
  229. if (n instanceof BranchNode && branches.includes(n.branch.name)) {
  230. await n.loadMore({ until: commit.ref });
  231. return true;
  232. }
  233. return false;
  234. },
  235. token: token,
  236. });
  237. }
  238. @gate(() => '')
  239. revealBranch(
  240. branch: GitBranchReference,
  241. options?: {
  242. select?: boolean;
  243. focus?: boolean;
  244. expand?: boolean | number;
  245. },
  246. ) {
  247. return window.withProgress(
  248. {
  249. location: ProgressLocation.Notification,
  250. title: `Revealing ${GitReference.toString(branch, { icon: false, quoted: true })} in the side bar...`,
  251. cancellable: true,
  252. },
  253. async (progress, token) => {
  254. const node = await this.findBranch(branch, token);
  255. if (node == null) return undefined;
  256. await this.ensureRevealNode(node, options);
  257. return node;
  258. },
  259. );
  260. }
  261. @gate(() => '')
  262. async revealCommit(
  263. commit: GitRevisionReference,
  264. options?: {
  265. select?: boolean;
  266. focus?: boolean;
  267. expand?: boolean | number;
  268. },
  269. ) {
  270. return window.withProgress(
  271. {
  272. location: ProgressLocation.Notification,
  273. title: `Revealing ${GitReference.toString(commit, { icon: false, quoted: true })} in the side bar...`,
  274. cancellable: true,
  275. },
  276. async (progress, token) => {
  277. const node = await this.findCommit(commit, token);
  278. if (node == null) return undefined;
  279. await this.ensureRevealNode(node, options);
  280. return node;
  281. },
  282. );
  283. }
  284. @gate(() => '')
  285. async revealRepository(
  286. repoPath: string,
  287. options?: { select?: boolean; focus?: boolean; expand?: boolean | number },
  288. ) {
  289. const node = await this.findNode(RepositoryFolderNode.getId(repoPath), {
  290. maxDepth: 1,
  291. canTraverse: n => n instanceof BranchesViewNode || n instanceof RepositoryFolderNode,
  292. });
  293. if (node !== undefined) {
  294. await this.reveal(node, options);
  295. }
  296. return node;
  297. }
  298. private setLayout(layout: ViewBranchesLayout) {
  299. return configuration.updateEffective(`views.${this.configKey}.branches.layout` as const, layout);
  300. }
  301. private setFilesLayout(layout: ViewFilesLayout) {
  302. return configuration.updateEffective(`views.${this.configKey}.files.layout` as const, layout);
  303. }
  304. private setShowAvatars(enabled: boolean) {
  305. return configuration.updateEffective(`views.${this.configKey}.avatars` as const, enabled);
  306. }
  307. private setShowBranchComparison(enabled: boolean) {
  308. return configuration.updateEffective(
  309. `views.${this.configKey}.showBranchComparison` as const,
  310. enabled ? ViewShowBranchComparison.Branch : false,
  311. );
  312. }
  313. private async setShowBranchPullRequest(enabled: boolean) {
  314. await configuration.updateEffective(`views.${this.configKey}.pullRequests.showForBranches` as const, enabled);
  315. await configuration.updateEffective(`views.${this.configKey}.pullRequests.enabled` as const, enabled);
  316. }
  317. }