git-view.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. const _ = require("underscore-plus");
  2. const { CompositeDisposable, GitRepositoryAsync } = require("atom");
  3. module.exports =
  4. class GitView {
  5. constructor() {
  6. this.element = document.createElement('status-bar-git');
  7. this.element.classList.add('git-view');
  8. this.createBranchArea();
  9. this.createCommitsArea();
  10. this.createStatusArea();
  11. this.activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem(() => {
  12. this.subscribeToActiveItem();
  13. });
  14. this.projectPathSubscription = atom.project.onDidChangePaths(() => {
  15. this.subscribeToRepositories();
  16. });
  17. this.subscribeToRepositories();
  18. this.subscribeToActiveItem();
  19. }
  20. createBranchArea() {
  21. this.branchArea = document.createElement('div');
  22. this.branchArea.classList.add('git-branch', 'inline-block');
  23. this.element.appendChild(this.branchArea);
  24. this.element.branchArea = this.branchArea;
  25. const branchIcon = document.createElement('span');
  26. branchIcon.classList.add('icon', 'icon-git-branch');
  27. this.branchArea.appendChild(branchIcon);
  28. this.branchLabel = document.createElement('span');
  29. this.branchLabel.classList.add('branch-label');
  30. this.branchArea.appendChild(this.branchLabel);
  31. this.element.branchLabel = this.branchLabel;
  32. }
  33. createCommitsArea() {
  34. this.commitsArea = document.createElement('div');
  35. this.commitsArea.classList.add('git-commits', 'inline-block');
  36. this.element.appendChild(this.commitsArea);
  37. this.commitsAhead = document.createElement('span');
  38. this.commitsAhead.classList.add('icon', 'icon-arrow-up', 'commits-ahead-label');
  39. this.commitsArea.appendChild(this.commitsAhead);
  40. this.commitsBehind = document.createElement('span');
  41. this.commitsBehind.classList.add('icon', 'icon-arrow-down', 'commits-behind-label');
  42. this.commitsArea.appendChild(this.commitsBehind);
  43. }
  44. createStatusArea() {
  45. this.gitStatus = document.createElement('div');
  46. this.gitStatus.classList.add('git-status', 'inline-block');
  47. this.element.appendChild(this.gitStatus);
  48. this.gitStatusIcon = document.createElement('span');
  49. this.gitStatusIcon.classList.add('icon');
  50. this.gitStatus.appendChild(this.gitStatusIcon);
  51. this.element.gitStatusIcon = this.gitStatusIcon;
  52. }
  53. subscribeToActiveItem() {
  54. const activeItem = this.getActiveItem();
  55. this.savedSubscription?.dispose();
  56. this.savedSubscription = activeItem?.onDidSave?.(() => this.update());
  57. this.update();
  58. }
  59. subscribeToRepositories() {
  60. this.repositorySubscriptions?.dispose();
  61. this.repositorySubscriptions = new CompositeDisposable;
  62. const result = [];
  63. for (let repo of atom.project.getRepositories()) {
  64. if (repo != null) {
  65. this.repositorySubscriptions.add(
  66. repo.onDidChangeStatus(({ path, status }) => {
  67. if (path === this.getActiveItemPath()) {
  68. this.update();
  69. }
  70. })
  71. );
  72. result.push(this.repositorySubscriptions.add(
  73. repo.onDidChangeStatuses(() => {
  74. this.update();
  75. })
  76. ));
  77. }
  78. }
  79. return result;
  80. }
  81. destroy() {
  82. this.activeItemSubscription?.dispose();
  83. this.projectPathSubscription?.dispose();
  84. this.savedSubscription?.dispose();
  85. this.repositorySubscriptions?.dispose();
  86. this.branchTooltipDisposable?.dispose();
  87. this.commitsAheadTooltipDisposable?.dispose();
  88. this.commitsBehindTooltipDisposable?.dispose();
  89. this.statusTooltipDisposable?.dispose();
  90. }
  91. getActiveItemPath() {
  92. return this.getActiveItem()?.getPath?.();
  93. }
  94. getRepositoryForActiveItem() {
  95. const [rootDir] = atom.project.relativizePath(this.getActiveItemPath());
  96. const rootDirIndex = atom.project.getPaths().indexOf(rootDir);
  97. if (rootDirIndex >= 0) {
  98. return atom.project.getRepositories()[rootDirIndex];
  99. } else {
  100. for (let repo of atom.project.getRepositories()) {
  101. if (repo) {
  102. return repo;
  103. }
  104. }
  105. }
  106. }
  107. getActiveItem() {
  108. return atom.workspace.getCenter().getActivePaneItem();
  109. }
  110. update() {
  111. const repo = this.getRepositoryForActiveItem();
  112. this.updateBranchText(repo);
  113. this.updateAheadBehindCount(repo);
  114. this.updateStatusText(repo);
  115. }
  116. updateBranchText(repo) {
  117. if (this.showGitInformation(repo)) {
  118. const head = repo.getShortHead(this.getActiveItemPath());
  119. this.branchLabel.textContent = head;
  120. if (head) { this.branchArea.style.display = ''; }
  121. this.branchTooltipDisposable?.dispose();
  122. this.branchTooltipDisposable = atom.tooltips.add(this.branchArea, {title: `On branch ${head}`});
  123. } else {
  124. this.branchArea.style.display = 'none';
  125. }
  126. }
  127. showGitInformation(repo) {
  128. if (repo == null) { return false; }
  129. const itemPath = this.getActiveItemPath();
  130. if (itemPath) {
  131. return atom.project.contains(itemPath);
  132. } else {
  133. return (this.getActiveItem() == null);
  134. }
  135. }
  136. updateAheadBehindCount(repo) {
  137. if (!this.showGitInformation(repo)) {
  138. this.commitsArea.style.display = 'none';
  139. return;
  140. }
  141. const itemPath = this.getActiveItemPath();
  142. const {ahead, behind} = repo.getCachedUpstreamAheadBehindCount(itemPath);
  143. if (ahead > 0) {
  144. this.commitsAhead.textContent = ahead;
  145. this.commitsAhead.style.display = '';
  146. this.commitsAheadTooltipDisposable?.dispose();
  147. this.commitsAheadTooltipDisposable = atom.tooltips.add(this.commitsAhead, {title: `${_.pluralize(ahead, 'commit')} ahead of upstream`});
  148. } else {
  149. this.commitsAhead.style.display = 'none';
  150. }
  151. if (behind > 0) {
  152. this.commitsBehind.textContent = behind;
  153. this.commitsBehind.style.display = '';
  154. this.commitsBehindTooltipDisposable?.dispose();
  155. this.commitsBehindTooltipDisposable = atom.tooltips.add(this.commitsBehind, {title: `${_.pluralize(behind, 'commit')} behind upstream`});
  156. } else {
  157. this.commitsBehind.style.display = 'none';
  158. }
  159. if ((ahead > 0) || (behind > 0)) {
  160. this.commitsArea.style.display = '';
  161. } else {
  162. this.commitsArea.style.display = 'none';
  163. }
  164. }
  165. clearStatus() {
  166. this.gitStatusIcon.classList.remove('icon-diff-modified', 'status-modified', 'icon-diff-added', 'status-added', 'icon-diff-ignored', 'status-ignored');
  167. }
  168. updateAsNewFile() {
  169. this.clearStatus();
  170. const textEditor = atom.workspace.getActiveTextEditor();
  171. this.gitStatusIcon.classList.add('icon-diff-added', 'status-added');
  172. if (textEditor) {
  173. this.gitStatusIcon.textContent = `+${textEditor.getLineCount()}`;
  174. this.updateTooltipText(`${_.pluralize(textEditor.getLineCount(), 'line')} in this new file not yet committed`);
  175. } else {
  176. this.gitStatusIcon.textContent = '';
  177. this.updateTooltipText();
  178. }
  179. this.gitStatus.style.display = '';
  180. }
  181. updateAsModifiedFile(repo, path) {
  182. const stats = repo.getDiffStats(path);
  183. this.clearStatus();
  184. this.gitStatusIcon.classList.add('icon-diff-modified', 'status-modified');
  185. if (stats.added && stats.deleted) {
  186. this.gitStatusIcon.textContent = `+${stats.added}, -${stats.deleted}`;
  187. this.updateTooltipText(`${_.pluralize(stats.added, 'line')} added and ${_.pluralize(stats.deleted, 'line')} deleted in this file not yet committed`);
  188. } else if (stats.added) {
  189. this.gitStatusIcon.textContent = `+${stats.added}`;
  190. this.updateTooltipText(`${_.pluralize(stats.added, 'line')} added to this file not yet committed`);
  191. } else if (stats.deleted) {
  192. this.gitStatusIcon.textContent = `-${stats.deleted}`;
  193. this.updateTooltipText(`${_.pluralize(stats.deleted, 'line')} deleted from this file not yet committed`);
  194. } else {
  195. this.gitStatusIcon.textContent = '';
  196. this.updateTooltipText();
  197. }
  198. this.gitStatus.style.display = '';
  199. }
  200. updateAsIgnoredFile() {
  201. this.clearStatus();
  202. this.gitStatusIcon.classList.add('icon-diff-ignored', 'status-ignored');
  203. this.gitStatusIcon.textContent = '';
  204. this.gitStatus.style.display = '';
  205. this.updateTooltipText("File is ignored by git");
  206. }
  207. updateTooltipText(text) {
  208. this.statusTooltipDisposable?.dispose();
  209. if (text) {
  210. this.statusTooltipDisposable = atom.tooltips.add(this.gitStatusIcon, {title: text});
  211. }
  212. }
  213. updateStatusText(repo) {
  214. const hideStatus = () => {
  215. this.clearStatus();
  216. this.gitStatus.style.display = 'none';
  217. };
  218. const itemPath = this.getActiveItemPath();
  219. if (this.showGitInformation(repo) && (itemPath != null)) {
  220. const status = repo.getCachedPathStatus(itemPath) ?? 0;
  221. if (repo.isStatusNew(status)) {
  222. return this.updateAsNewFile();
  223. }
  224. if (repo.isStatusModified(status)) {
  225. return this.updateAsModifiedFile(repo, itemPath);
  226. }
  227. if (repo.isPathIgnored(itemPath)) {
  228. return this.updateAsIgnoredFile();
  229. } else {
  230. return hideStatus();
  231. }
  232. } else {
  233. return hideStatus();
  234. }
  235. }
  236. }