git-repository-spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. const path = require('path');
  2. const fs = require('fs-plus');
  3. const temp = require('temp').track();
  4. const GitRepository = require('../src/git-repository');
  5. const Project = require('../src/project');
  6. describe('GitRepository', () => {
  7. let repo;
  8. beforeEach(() => {
  9. const gitPath = path.join(temp.dir, '.git');
  10. if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath);
  11. });
  12. afterEach(() => {
  13. if (repo && !repo.isDestroyed()) repo.destroy();
  14. });
  15. describe('@open(path)', () => {
  16. it('returns null when no repository is found', () => {
  17. expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull();
  18. });
  19. });
  20. describe('new GitRepository(path)', () => {
  21. it('throws an exception when no repository is found', () => {
  22. expect(
  23. () => new GitRepository(path.join(temp.dir, 'nogit.txt'))
  24. ).toThrow();
  25. });
  26. });
  27. describe('.getPath()', () => {
  28. it('returns the repository path for a .git directory path with a directory', () => {
  29. repo = new GitRepository(
  30. path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')
  31. );
  32. expect(repo.getPath()).toBe(
  33. path.join(__dirname, 'fixtures', 'git', 'master.git')
  34. );
  35. });
  36. it('returns the repository path for a repository path', () => {
  37. repo = new GitRepository(
  38. path.join(__dirname, 'fixtures', 'git', 'master.git')
  39. );
  40. expect(repo.getPath()).toBe(
  41. path.join(__dirname, 'fixtures', 'git', 'master.git')
  42. );
  43. });
  44. });
  45. describe('.isPathIgnored(path)', () => {
  46. it('returns true for an ignored path', () => {
  47. repo = new GitRepository(
  48. path.join(__dirname, 'fixtures', 'git', 'ignore.git')
  49. );
  50. expect(repo.isPathIgnored('a.txt')).toBeTruthy();
  51. });
  52. it('returns false for a non-ignored path', () => {
  53. repo = new GitRepository(
  54. path.join(__dirname, 'fixtures', 'git', 'ignore.git')
  55. );
  56. expect(repo.isPathIgnored('b.txt')).toBeFalsy();
  57. });
  58. });
  59. describe('.isPathModified(path)', () => {
  60. let filePath, newPath;
  61. beforeEach(() => {
  62. const workingDirPath = copyRepository();
  63. repo = new GitRepository(workingDirPath);
  64. filePath = path.join(workingDirPath, 'a.txt');
  65. newPath = path.join(workingDirPath, 'new-path.txt');
  66. });
  67. describe('when the path is unstaged', () => {
  68. it('returns false if the path has not been modified', () => {
  69. expect(repo.isPathModified(filePath)).toBeFalsy();
  70. });
  71. it('returns true if the path is modified', () => {
  72. fs.writeFileSync(filePath, 'change');
  73. expect(repo.isPathModified(filePath)).toBeTruthy();
  74. });
  75. it('returns true if the path is deleted', () => {
  76. fs.removeSync(filePath);
  77. expect(repo.isPathModified(filePath)).toBeTruthy();
  78. });
  79. it('returns false if the path is new', () => {
  80. expect(repo.isPathModified(newPath)).toBeFalsy();
  81. });
  82. });
  83. });
  84. describe('.isPathNew(path)', () => {
  85. let filePath, newPath;
  86. beforeEach(() => {
  87. const workingDirPath = copyRepository();
  88. repo = new GitRepository(workingDirPath);
  89. filePath = path.join(workingDirPath, 'a.txt');
  90. newPath = path.join(workingDirPath, 'new-path.txt');
  91. fs.writeFileSync(newPath, "i'm new here");
  92. });
  93. describe('when the path is unstaged', () => {
  94. it('returns true if the path is new', () => {
  95. expect(repo.isPathNew(newPath)).toBeTruthy();
  96. });
  97. it("returns false if the path isn't new", () => {
  98. expect(repo.isPathNew(filePath)).toBeFalsy();
  99. });
  100. });
  101. });
  102. describe('.checkoutHead(path)', () => {
  103. let filePath;
  104. beforeEach(() => {
  105. const workingDirPath = copyRepository();
  106. repo = new GitRepository(workingDirPath);
  107. filePath = path.join(workingDirPath, 'a.txt');
  108. });
  109. it('no longer reports a path as modified after checkout', () => {
  110. expect(repo.isPathModified(filePath)).toBeFalsy();
  111. fs.writeFileSync(filePath, 'ch ch changes');
  112. expect(repo.isPathModified(filePath)).toBeTruthy();
  113. expect(repo.checkoutHead(filePath)).toBeTruthy();
  114. expect(repo.isPathModified(filePath)).toBeFalsy();
  115. });
  116. it('restores the contents of the path to the original text', () => {
  117. fs.writeFileSync(filePath, 'ch ch changes');
  118. expect(repo.checkoutHead(filePath)).toBeTruthy();
  119. expect(fs.readFileSync(filePath, 'utf8')).toBe('');
  120. });
  121. it('fires a status-changed event if the checkout completes successfully', () => {
  122. fs.writeFileSync(filePath, 'ch ch changes');
  123. repo.getPathStatus(filePath);
  124. const statusHandler = jasmine.createSpy('statusHandler');
  125. repo.onDidChangeStatus(statusHandler);
  126. repo.checkoutHead(filePath);
  127. expect(statusHandler.callCount).toBe(1);
  128. expect(statusHandler.argsForCall[0][0]).toEqual({
  129. path: filePath,
  130. pathStatus: 0
  131. });
  132. repo.checkoutHead(filePath);
  133. expect(statusHandler.callCount).toBe(1);
  134. });
  135. });
  136. describe('.checkoutHeadForEditor(editor)', () => {
  137. let filePath, editor;
  138. beforeEach(async () => {
  139. spyOn(atom, 'confirm');
  140. const workingDirPath = copyRepository();
  141. repo = new GitRepository(workingDirPath, {
  142. project: atom.project,
  143. config: atom.config,
  144. confirm: atom.confirm
  145. });
  146. filePath = path.join(workingDirPath, 'a.txt');
  147. fs.writeFileSync(filePath, 'ch ch changes');
  148. editor = await atom.workspace.open(filePath);
  149. });
  150. it('displays a confirmation dialog by default', () => {
  151. // Permissions issues with this test on Windows
  152. if (process.platform === 'win32') return;
  153. atom.confirm.andCallFake(({ buttons }) => buttons.OK());
  154. atom.config.set('editor.confirmCheckoutHeadRevision', true);
  155. repo.checkoutHeadForEditor(editor);
  156. expect(fs.readFileSync(filePath, 'utf8')).toBe('');
  157. });
  158. it('does not display a dialog when confirmation is disabled', () => {
  159. // Flakey EPERM opening a.txt on Win32
  160. if (process.platform === 'win32') return;
  161. atom.config.set('editor.confirmCheckoutHeadRevision', false);
  162. repo.checkoutHeadForEditor(editor);
  163. expect(fs.readFileSync(filePath, 'utf8')).toBe('');
  164. expect(atom.confirm).not.toHaveBeenCalled();
  165. });
  166. });
  167. describe('.destroy()', () => {
  168. it('throws an exception when any method is called after it is called', () => {
  169. repo = new GitRepository(
  170. path.join(__dirname, 'fixtures', 'git', 'master.git')
  171. );
  172. repo.destroy();
  173. expect(() => repo.getShortHead()).toThrow();
  174. });
  175. });
  176. describe('.getPathStatus(path)', () => {
  177. let filePath;
  178. beforeEach(() => {
  179. const workingDirectory = copyRepository();
  180. repo = new GitRepository(workingDirectory);
  181. filePath = path.join(workingDirectory, 'file.txt');
  182. });
  183. it('trigger a status-changed event when the new status differs from the last cached one', () => {
  184. const statusHandler = jasmine.createSpy('statusHandler');
  185. repo.onDidChangeStatus(statusHandler);
  186. fs.writeFileSync(filePath, '');
  187. let status = repo.getPathStatus(filePath);
  188. expect(statusHandler.callCount).toBe(1);
  189. expect(statusHandler.argsForCall[0][0]).toEqual({
  190. path: filePath,
  191. pathStatus: status
  192. });
  193. fs.writeFileSync(filePath, 'abc');
  194. status = repo.getPathStatus(filePath);
  195. expect(statusHandler.callCount).toBe(1);
  196. });
  197. });
  198. describe('.getDirectoryStatus(path)', () => {
  199. let directoryPath, filePath;
  200. beforeEach(() => {
  201. const workingDirectory = copyRepository();
  202. repo = new GitRepository(workingDirectory);
  203. directoryPath = path.join(workingDirectory, 'dir');
  204. filePath = path.join(directoryPath, 'b.txt');
  205. });
  206. it('gets the status based on the files inside the directory', () => {
  207. expect(
  208. repo.isStatusModified(repo.getDirectoryStatus(directoryPath))
  209. ).toBe(false);
  210. fs.writeFileSync(filePath, 'abc');
  211. repo.getPathStatus(filePath);
  212. expect(
  213. repo.isStatusModified(repo.getDirectoryStatus(directoryPath))
  214. ).toBe(true);
  215. });
  216. });
  217. describe('.refreshStatus()', () => {
  218. let newPath, modifiedPath, cleanPath, workingDirectory;
  219. beforeEach(() => {
  220. workingDirectory = copyRepository();
  221. repo = new GitRepository(workingDirectory, {
  222. project: atom.project,
  223. config: atom.config
  224. });
  225. modifiedPath = path.join(workingDirectory, 'file.txt');
  226. newPath = path.join(workingDirectory, 'untracked.txt');
  227. cleanPath = path.join(workingDirectory, 'other.txt');
  228. fs.writeFileSync(cleanPath, 'Full of text');
  229. fs.writeFileSync(newPath, '');
  230. newPath = fs.absolute(newPath);
  231. });
  232. it('returns status information for all new and modified files', async () => {
  233. const statusHandler = jasmine.createSpy('statusHandler');
  234. repo.onDidChangeStatuses(statusHandler);
  235. fs.writeFileSync(modifiedPath, 'making this path modified');
  236. await repo.refreshStatus();
  237. expect(statusHandler.callCount).toBe(1);
  238. expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined();
  239. expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy();
  240. expect(
  241. repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))
  242. ).toBeTruthy();
  243. });
  244. it('caches the proper statuses when a subdir is open', async () => {
  245. const subDir = path.join(workingDirectory, 'dir');
  246. fs.mkdirSync(subDir);
  247. const filePath = path.join(subDir, 'b.txt');
  248. fs.writeFileSync(filePath, '');
  249. atom.project.setPaths([subDir]);
  250. await atom.workspace.open('b.txt');
  251. repo = atom.project.getRepositories()[0];
  252. await repo.refreshStatus();
  253. const status = repo.getCachedPathStatus(filePath);
  254. expect(repo.isStatusModified(status)).toBe(false);
  255. expect(repo.isStatusNew(status)).toBe(false);
  256. });
  257. it('works correctly when the project has multiple folders (regression)', async () => {
  258. atom.project.addPath(workingDirectory);
  259. atom.project.addPath(path.join(__dirname, 'fixtures', 'dir'));
  260. await repo.refreshStatus();
  261. expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined();
  262. expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy();
  263. expect(
  264. repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))
  265. ).toBeTruthy();
  266. });
  267. it('caches statuses that were looked up synchronously', async () => {
  268. const originalContent = 'undefined';
  269. fs.writeFileSync(modifiedPath, 'making this path modified');
  270. repo.getPathStatus('file.txt');
  271. fs.writeFileSync(modifiedPath, originalContent);
  272. await repo.refreshStatus();
  273. expect(
  274. repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))
  275. ).toBeFalsy();
  276. });
  277. });
  278. describe('buffer events', () => {
  279. let editor;
  280. beforeEach(async () => {
  281. atom.project.setPaths([copyRepository()]);
  282. const refreshPromise = new Promise(resolve =>
  283. atom.project.getRepositories()[0].onDidChangeStatuses(resolve)
  284. );
  285. editor = await atom.workspace.open('other.txt');
  286. await refreshPromise;
  287. });
  288. it('emits a status-changed event when a buffer is saved', async () => {
  289. editor.insertNewline();
  290. const statusHandler = jasmine.createSpy('statusHandler');
  291. atom.project.getRepositories()[0].onDidChangeStatus(statusHandler);
  292. await editor.save();
  293. expect(statusHandler.callCount).toBe(1);
  294. expect(statusHandler).toHaveBeenCalledWith({
  295. path: editor.getPath(),
  296. pathStatus: 256
  297. });
  298. });
  299. it('emits a status-changed event when a buffer is reloaded', async () => {
  300. fs.writeFileSync(editor.getPath(), 'changed');
  301. const statusHandler = jasmine.createSpy('statusHandler');
  302. atom.project.getRepositories()[0].onDidChangeStatus(statusHandler);
  303. await editor.getBuffer().reload();
  304. expect(statusHandler.callCount).toBe(1);
  305. expect(statusHandler).toHaveBeenCalledWith({
  306. path: editor.getPath(),
  307. pathStatus: 256
  308. });
  309. await editor.getBuffer().reload();
  310. expect(statusHandler.callCount).toBe(1);
  311. });
  312. it("emits a status-changed event when a buffer's path changes", () => {
  313. fs.writeFileSync(editor.getPath(), 'changed');
  314. const statusHandler = jasmine.createSpy('statusHandler');
  315. atom.project.getRepositories()[0].onDidChangeStatus(statusHandler);
  316. editor.getBuffer().emitter.emit('did-change-path');
  317. expect(statusHandler.callCount).toBe(1);
  318. expect(statusHandler).toHaveBeenCalledWith({
  319. path: editor.getPath(),
  320. pathStatus: 256
  321. });
  322. editor.getBuffer().emitter.emit('did-change-path');
  323. expect(statusHandler.callCount).toBe(1);
  324. });
  325. it('stops listening to the buffer when the repository is destroyed (regression)', () => {
  326. atom.project.getRepositories()[0].destroy();
  327. expect(() => editor.save()).not.toThrow();
  328. });
  329. });
  330. describe('when a project is deserialized', () => {
  331. let buffer, project2, statusHandler;
  332. afterEach(() => {
  333. if (project2) project2.destroy();
  334. });
  335. it('subscribes to all the serialized buffers in the project', async () => {
  336. atom.project.setPaths([copyRepository()]);
  337. await atom.workspace.open('file.txt');
  338. project2 = new Project({
  339. notificationManager: atom.notifications,
  340. packageManager: atom.packages,
  341. confirm: atom.confirm,
  342. grammarRegistry: atom.grammars,
  343. applicationDelegate: atom.applicationDelegate
  344. });
  345. await project2.deserialize(
  346. atom.project.serialize({ isUnloading: false })
  347. );
  348. buffer = project2.getBuffers()[0];
  349. buffer.append('changes');
  350. statusHandler = jasmine.createSpy('statusHandler');
  351. project2.getRepositories()[0].onDidChangeStatus(statusHandler);
  352. await buffer.save();
  353. expect(statusHandler.callCount).toBe(1);
  354. expect(statusHandler).toHaveBeenCalledWith({
  355. path: buffer.getPath(),
  356. pathStatus: 256
  357. });
  358. });
  359. });
  360. });
  361. function copyRepository() {
  362. const workingDirPath = temp.mkdirSync('atom-spec-git');
  363. fs.copySync(
  364. path.join(__dirname, 'fixtures', 'git', 'working-dir'),
  365. workingDirPath
  366. );
  367. fs.renameSync(
  368. path.join(workingDirPath, 'git.git'),
  369. path.join(workingDirPath, '.git')
  370. );
  371. return workingDirPath;
  372. }