コード例 #1
0
  // Loads the basic grammar structure,
  // which includes the grouped parts in the repository,
  // and then loads all grammar subrepositories,
  // and appends them to the main repository,
  // and finally writes {grammar} to {output}
  compile () {
    const input = '../grammars/repositories/markdown.cson'
    const output = '../grammars/language-markdown.json'
    const directories = ['blocks', 'flavors', 'inlines']
    const inputPath = path.join(__dirname, input)
    const grammar = CSON.readFileSync(inputPath)

    for (let i = 0; i < directories.length; i++) {
      const directoryPath = path.join(__dirname, '../grammars/repositories/' + directories[i])
      const directory = new Directory(directoryPath)
      const entries = directory.getEntriesSync()
      for (let j = 0; j < entries.length; j++) {
        const entry = entries[j];
        const { key, patterns } = CSON.readFileSync(entry.path)
        if (key && patterns) {
          grammar.repository[key] = { patterns }
        }
      }
    }

    grammar.repository['fenced-code-blocks'] = {
      patterns: this.compileFencedCodeGrammar()
    }

    const outputPath = path.join(__dirname, output)
    CSON.writeFileSync(outputPath, grammar, (function () {
      return atom.commands.dispatch('body', 'window:reload')
    })())
  }
コード例 #2
0
    return trackTiming('hg-repository.repositoryForDirectorySync', () => {
      try {
        const repositoryDescription = getRepositoryDescription(directory);
        if (!repositoryDescription) {
          return null;
        }

        const {
          originURL,
          repoPath,
          workingDirectory,
          workingDirectoryLocalPath,
        } = repositoryDescription;

        const service = getHgServiceByNuclideUri(directory.getPath());
        const hgService = new service.HgService(workingDirectoryLocalPath);
        return new HgRepositoryClient(repoPath, hgService, {
          workingDirectory,
          projectRootDirectory: directory,
          originURL,
        });
      } catch (err) {
        logger.error(
          'Failed to create an HgRepositoryClient for ', directory.getPath(), ', error: ', err,
        );
        return null;
      }
    });
コード例 #3
0
    beforeEach(() => {
      // Unfortunately, when the temp files in these tests are opened in the editor,
      // editor.getPath() returns the original file path with '/private/' appended
      // to it. Thus, the path returned from editor.getPath() (which is what is
      // used in HgRepository) would fail a real 'contains' method. So we override
      // this to the expected path.
      featureConfig.set('nuclide-hg-repository.enableDiffStats', true);
      const workingDirectoryClone = new Directory(tempDir);
      spyOn(workingDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return workingDirectoryClone.contains(prefixRemovedPath);
        }
        return workingDirectoryClone.contains(filePath);
      });

      const projectDirectoryClone = new Directory(tempSubDir);
      spyOn(projectDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return projectDirectoryClone.contains(prefixRemovedPath);
        }
        return projectDirectoryClone.contains(filePath);
      });

      spyOn(repo, '_updateDiffInfo').andReturn(Observable.of('fake'));
      spyOn(repo, '_observePaneItemVisibility').andReturn(Observable.of(true));
    });
コード例 #4
0
  repositoryForDirectorySync(directory: Directory): ?HgRepository {
    try {
      var repositoryDescription = getRepositoryDescription(directory);
      if (!repositoryDescription) {
        return null;
      }

      var {originURL, repoPath, workingDirectory, workingDirectoryLocalPath} = repositoryDescription;

      var {getServiceByNuclideUri} = require('nuclide-client');
      var service = getServiceByNuclideUri(
        'HgService',
        directory.getPath(),
        {workingDirectory: workingDirectoryLocalPath}
      );
      var {HgRepositoryClient} = require('nuclide-hg-repository-client');
      return new HgRepositoryClient(repoPath, service, {
        workingDirectory,
        projectRootDirectory: directory,
        originURL,
      });
    } catch (err) {
      getLogger().error('Failed to create an HgRepositoryClient for ', directory.getPath(), ', error: ', err);
      return null;
    }
  }
コード例 #5
0
    beforeEach(() => {
      // Unfortunately, when the temp files in these tests are opened in the editor,
      // editor.getPath() returns the original file path with '/private/' appended
      // to it. Thus, the path returned from editor.getPath() (which is what is
      // used in HgRepository) would fail a real 'contains' method. So we override
      // this to the expected path.
      const workingDirectoryClone = new Directory(tempDir);
      spyOn(workingDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return workingDirectoryClone.contains(prefixRemovedPath);
        }
        return workingDirectoryClone.contains(filePath);
      });

      const projectDirectoryClone = new Directory(tempSubDir);
      spyOn(projectDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return projectDirectoryClone.contains(prefixRemovedPath);
        }
        return projectDirectoryClone.contains(filePath);
      });
    });
コード例 #6
0
 spyOn(projectDirectory, 'contains').andCallFake(filePath => {
   const prefix = '/private';
   if (filePath.startsWith(prefix)) {
     const prefixRemovedPath = filePath.slice(prefix.length);
     return projectDirectoryClone.contains(prefixRemovedPath);
   }
   return projectDirectoryClone.contains(filePath);
 });
コード例 #7
0
 crisper: function(path) {
   if (!path) {
     return;
   }
   var dir = new Directory(path);
   dir.exists().then((exists) => {
     if (!exists) {
       return;
     }
     PolymerFile._crisperDir(dir);
   });
 },
コード例 #8
0
/**
 * @param repository Either a GitRepository or HgRepositoryClient.
 * @param filePath The absolute file path of interest.
 * @return boolean Whether the file path exists within the working directory
 *   (aka root directory) of the repository, or is the working directory.
 */
function repositoryContainsPath(repository: GitRepository | HgRepositoryClient, filePath: string): boolean {
  var workingDirectoryPath = repository.getWorkingDirectory();
  if (pathsAreEqual(workingDirectoryPath, filePath)) {
    return true;
  }

  if (repository.getType() === 'git') {
    var rootGitProjectDirectory = new Directory(workingDirectoryPath);
    return rootGitProjectDirectory.contains(filePath);
  } else if (repository.getType() === 'hg') {
    return repository._workingDirectory.contains(filePath);
  }
  throw new Error('repositoryContainsPath: Received an unrecognized repository type. Expected git or hg.');
}
コード例 #9
0
/**
 * @param repository Either a GitRepository or HgRepositoryClient.
 * @param filePath The absolute file path of interest.
 * @return boolean Whether the file path exists within the working directory
 *   (aka root directory) of the repository, or is the working directory.
 */
function repositoryContainsPath(repository: atom$Repository, filePath: NuclideUri): boolean {
  const workingDirectoryPath = repository.getWorkingDirectory();
  if (pathsAreEqual(workingDirectoryPath, filePath)) {
    return true;
  }

  if (repository.getType() === 'git') {
    const rootGitProjectDirectory = new Directory(workingDirectoryPath);
    return rootGitProjectDirectory.contains(filePath);
  } else if (repository.getType() === 'hg') {
    const hgRepository = ((repository: any): HgRepositoryClient);
    return hgRepository._workingDirectory.contains(filePath);
  }
  throw new Error(
    'repositoryContainsPath: Received an unrecognized repository type. Expected git or hg.');
}
コード例 #10
0
   + ' project.', () => {
   spyOn(repo, '_updateDiffInfo');
   const file = temp.openSync({dir: projectDirectory.getPath()});
   waitsForPromise(async () => {
     const editor = await atom.workspace.open(file.path);
     expect(repo._updateDiffInfo.calls.length).toBe(1);
     expect(repo._updateDiffInfo).toHaveBeenCalledWith(editor.getPath());
   });
 });
コード例 #11
0
   + ' project.', () => {
   spyOn(repo, '_updateDiffInfo');
   const file = temp.openSync({dir: projectDirectory.getPath()});
   waitsForPromise(async () => {
     const editor = await atom.workspace.open(file.path);
     expect(repo._hgDiffCacheFilesToClear.size).toBe(0);
     editor.destroy();
     const expectedSet = new Set([editor.getPath()]);
     expect(repo._hgDiffCacheFilesToClear).toEqual(expectedSet);
   });
 });
コード例 #12
0
/**
 * @param directory Either a RemoteDirectory or Directory we are interested in.
 * @return If the directory is part of a Mercurial repository, returns an object
 *  with the following field:
 *  * originURL The string URL of the repository origin.
 *  * repoPath The path/uri to the repository (.hg folder).
 *  * workingDirectory A Directory (or RemoteDirectory) object that represents
 *    the repository's working directory.
 *  * workingDirectoryLocalPath The local path to the workingDirectory of the
 *    repository (i.e. if it's a remote directory, the URI minus the hostname).
 *  If the directory is not part of a Mercurial repository, returns null.
 */
function getRepositoryDescription(directory: Directory): ?mixed {
  var {RemoteDirectory} = require('nuclide-remote-connection');

  if (RemoteDirectory.isRemoteDirectory(directory)) {
    var repositoryDescription = directory.getHgRepositoryDescription();
    if (!repositoryDescription.repoPath) {
      return null;
    }

    var remoteConnection = directory._remote;
    var {repoPath, originURL, workingDirectoryPath} = repositoryDescription;
    var workingDirectoryLocalPath = workingDirectoryPath;
    // These paths are all relative to the remote fs. We need to turn these into URIs.
    var repoPath = remoteConnection.getUriOfRemotePath(repoPath);
    var workingDirectoryPath = remoteConnection.getUriOfRemotePath(workingDirectoryPath);
    return {
      originURL,
      repoPath,
      workingDirectory: new RemoteDirectory(remoteConnection, workingDirectoryPath),
      workingDirectoryLocalPath,
    };
  } else {
    var {findHgRepository} = require('nuclide-source-control-helpers');
    var repositoryDescription = findHgRepository(directory.getPath());
    if (!repositoryDescription.repoPath) {
      return null;
    }

    var {repoPath, originURL, workingDirectoryPath} = repositoryDescription;
    return {
      originURL,
      repoPath,
      workingDirectory: new Directory(workingDirectoryPath),
      workingDirectoryLocalPath: workingDirectoryPath,
    };
  }
}
コード例 #13
0
 it('should give all the files and subdirectories for the directory', () => {
   const directory = new Directory(`${rootDirectory}/spec/features`);
   const searcher = new FileSearch();
   return new Promise((resolve, reject) => {
     async.waterfall([
       next => directory.getEntries(next),
       (entries, next) => searcher.separateFilesAndDirectories(entries, next)
     ], (err, files, directories) => {
       if(err){
         return reject();
       }
       expect(files.length).toEqual(1);
       expect(directories.length).toEqual(2)
       resolve();
     });
   });
 });
コード例 #14
0
 next => directory.getEntries(next),
コード例 #15
0
 () => {
   expect(repo.getProjectDirectory()).toBe(projectDirectory.getPath());
 },
コード例 #16
0
 const createFilePath = filename => {
   return nuclideUri.join(projectDirectory.getPath(), filename);
 };
コード例 #17
0
ファイル: watcher.js プロジェクト: RobbyKetchell/atom
 watchDirectory (directoryPath) {
   if (this.isInAsarArchive(directoryPath)) return
   const entity = new Directory(directoryPath)
   this.disposables.add(entity.onDidChange(() => this.loadAllStylesheets()))
   this.entities.push(entity)
 }
コード例 #18
0
describe('HgRepositoryClient', () => {
  const tempDir = temp.mkdirSync('testproj');
  const tempSubDir = temp.mkdirSync({dir: tempDir});

  const repoPath = nuclideUri.join(tempDir, '.hg');
  const workingDirectory = new Directory(tempDir);
  const projectDirectory = new Directory(tempSubDir);
  const repoOptions = {
    originURL: 'http://test.com/testproj',
    workingDirectory,
    projectRootDirectory: projectDirectory,
  };

  // Manufactures the absolute path of a file that should pass as being
  // within the repo.
  const createFilePath = filename => {
    return nuclideUri.join(projectDirectory.getPath(), filename);
  };

  // Some test "absolute" paths.
  const PATH_1 = createFilePath('test1.js');
  const PATH_2 = createFilePath('test2.js');
  const PATH_3 = createFilePath('test3.js');
  const PATH_4 = createFilePath('test4.js');
  const PATH_5 = createFilePath('test5.js');
  const PATH_6 = createFilePath('test6.js');
  const PATH_7 = createFilePath('test7.js');
  const PATH_CALLED_NULL = createFilePath('null');
  const PATH_CALLED_UNDEFINED = createFilePath('undefined');

  let mockHgService: HgServiceType = (null: any);
  let repo: HgRepositoryClient = (null: any);

  beforeEach(() => {
    mockHgService = ((new MockHgService(): any): HgServiceType);
    repo = new HgRepositoryClient(repoPath, mockHgService, repoOptions);
  });

  describe('::getType()', () => {
    it('returns "hg"', () => {
      expect(repo.getType()).toBe('hg');
    });
  });

  describe('::getProjectDirectory', () => {
    it('returns the path of the root project folder in Atom that this Client provides information'
    + ' about.', () => {
      expect(repo.getProjectDirectory()).toBe(projectDirectory.getPath());
    });
  });

  describe('::isPathIgnored', () => {
    it('returns true if the path is marked ignored in the cache.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_1]: StatusCodeId.IGNORED,
      };
      expect(repo.isPathIgnored(PATH_1)).toBe(true);
    });

    it('returns true if the path is, or is within, the .hg directory.', () => {
      expect(repo.isPathIgnored(repoPath)).toBe(true);
      expect(repo.isPathIgnored(nuclideUri.join(repoPath, 'blah'))).toBe(true);
    });

    it('returns false if the path is not in the cache and is not the .hg directory.', () => {
      expect(repo.isPathIgnored('/A/Random/Path')).toBe(false);
      const parsedPath = nuclideUri.parsePath(repoPath);
      expect(repo.isPathIgnored(parsedPath.root)).toBe(false);
      expect(repo.isPathIgnored(parsedPath.dir)).toBe(false);
    });

    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.IGNORED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.IGNORED,
      };
      expect(repo.isPathIgnored(null)).toBe(false);
      expect(repo.isPathIgnored(undefined)).toBe(false);
      expect(repo.isPathIgnored(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathIgnored(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathNew', () => {
    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.ADDED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.ADDED,
      };
      expect(repo.isPathNew(null)).toBe(false);
      expect(repo.isPathNew(undefined)).toBe(false);
      expect(repo.isPathNew(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathNew(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathModified', () => {
    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.MODIFIED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.MODIFIED,
      };
      expect(repo.isPathModified(null)).toBe(false);
      expect(repo.isPathModified(undefined)).toBe(false);
      expect(repo.isPathModified(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathModified(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathAdded', () => {
    it('returns false if the path is null, untracked, modified or deleted'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.ADDED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.ADDED,
        [PATH_1]: StatusCodeId.ADDED,
        [PATH_2]: StatusCodeId.CLEAN,
        [PATH_3]: StatusCodeId.IGNORED,
        [PATH_4]: StatusCodeId.MISSING,
        [PATH_5]: StatusCodeId.MODIFIED,
        [PATH_6]: StatusCodeId.REMOVED,
        [PATH_7]: StatusCodeId.UNTRACKED,
      };
      expect(repo.isPathAdded(null)).toBe(false);
      expect(repo.isPathAdded(undefined)).toBe(false);
      expect(repo.isPathAdded(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathAdded(PATH_CALLED_UNDEFINED)).toBe(true);
      expect(repo.isPathAdded(PATH_1)).toBe(true);
      expect(repo.isPathAdded(PATH_2)).toBe(false);
      expect(repo.isPathAdded(PATH_3)).toBe(false);
      expect(repo.isPathAdded(PATH_4)).toBe(false);
      expect(repo.isPathAdded(PATH_5)).toBe(false);
      expect(repo.isPathAdded(PATH_6)).toBe(false);
      expect(repo.isPathAdded(PATH_7)).toBe(false);
    });
  });

  describe('::isPathUntracked', () => {
    it('returns false if the path is null, untracked, modified or deleted'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.UNTRACKED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.UNTRACKED,
        [PATH_1]: StatusCodeId.UNTRACKED,
        [PATH_2]: StatusCodeId.CLEAN,
        [PATH_3]: StatusCodeId.IGNORED,
        [PATH_4]: StatusCodeId.MISSING,
        [PATH_5]: StatusCodeId.MODIFIED,
        [PATH_6]: StatusCodeId.REMOVED,
        [PATH_7]: StatusCodeId.ADDED,
      };
      expect(repo.isPathUntracked(null)).toBe(false);
      expect(repo.isPathUntracked(undefined)).toBe(false);
      expect(repo.isPathUntracked(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathUntracked(PATH_CALLED_UNDEFINED)).toBe(true);
      expect(repo.isPathUntracked(PATH_1)).toBe(true);
      expect(repo.isPathUntracked(PATH_2)).toBe(false);
      expect(repo.isPathUntracked(PATH_3)).toBe(false);
      expect(repo.isPathUntracked(PATH_4)).toBe(false);
      expect(repo.isPathUntracked(PATH_5)).toBe(false);
      expect(repo.isPathUntracked(PATH_6)).toBe(false);
      expect(repo.isPathUntracked(PATH_7)).toBe(false);
    });
  });

  describe('::getCachedPathStatus', () => {
    beforeEach(() => {
      repo._hgStatusCache = {
        [PATH_1]: StatusCodeId.MODIFIED,
        [PATH_2]: StatusCodeId.IGNORED,
      };
    });

    it('retrieves cached hg status.', () => {
      // Force the state of the cache.
      const status = repo.getCachedPathStatus(PATH_1);
      expect(repo.isStatusModified(status)).toBe(true);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('retrieves cached hg ignore status.', () => {
      const status = repo.getCachedPathStatus(PATH_2);
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('returns a clean status by default.', () => {
      const status = repo.getCachedPathStatus('path-not-in-cache');
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });
  });

  describe('the hgDiffCache', () => {
    beforeEach(() => {
      // Unfortunately, when the temp files in these tests are opened in the editor,
      // editor.getPath() returns the original file path with '/private/' appended
      // to it. Thus, the path returned from editor.getPath() (which is what is
      // used in HgRepository) would fail a real 'contains' method. So we override
      // this to the expected path.
      atom.config.set('nuclide.nuclide-hg-repository.enableDiffStats', true);
      const workingDirectoryClone = new Directory(tempDir);
      spyOn(workingDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return workingDirectoryClone.contains(prefixRemovedPath);
        }
        return workingDirectoryClone.contains(filePath);
      });

      const projectDirectoryClone = new Directory(tempSubDir);
      spyOn(projectDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return projectDirectoryClone.contains(prefixRemovedPath);
        }
        return projectDirectoryClone.contains(filePath);
      });
    });

    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('is updated when the active pane item changes to an editor, if the editor file is in the'
      + ' project.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync({dir: projectDirectory.getPath()});
      waitsForPromise(async () => {
        const editor = await atom.workspace.open(file.path);
        expect(repo._updateDiffInfo.calls.length).toBe(1);
        expect(repo._updateDiffInfo).toHaveBeenCalledWith(editor.getPath());
      });
    });

    it('is not updated when the active pane item changes to an editor whose file is not in the'
      + ' repo.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync();
      waitsForPromise(async () => {
        await atom.workspace.open(file.path);
        expect(repo._updateDiffInfo.calls.length).toBe(0);
      });
    });

    it('marks a file to be removed from the cache after its editor is closed, if the file is in the'
      + ' project.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync({dir: projectDirectory.getPath()});
      waitsForPromise(async () => {
        const editor = await atom.workspace.open(file.path);
        expect(repo._hgDiffCacheFilesToClear.size).toBe(0);
        editor.destroy();
        const expectedSet = new Set([editor.getPath()]);
        expect(repo._hgDiffCacheFilesToClear).toEqual(expectedSet);
      });
    });
  });

  describe('::_updateDiffInfo', () => {
    const mockDiffInfo = {
      added: 2,
      deleted: 11,
      lineDiffs: [{
        oldStart: 150,
        oldLines: 11,
        newStart: 150,
        newLines: 2,
      }],
    };

    beforeEach(() => {
      spyOn(repo._service, 'fetchDiffInfo').andCallFake(filePaths => {
        const mockFetchedPathToDiffInfo = new Map();
        for (const filePath of filePaths) {
          mockFetchedPathToDiffInfo.set(filePath, mockDiffInfo);
        }
        return Promise.resolve(mockFetchedPathToDiffInfo);
      });
      spyOn(workingDirectory, 'contains').andCallFake(() => {
        return true;
      });
    });

    it('updates the cache when the path to update is not already being updated.', () => {
      waitsForPromise(async () => {
        expect(repo._hgDiffCache[PATH_1]).toBeUndefined();
        await repo._updateDiffInfo([PATH_1]);
        expect(repo._hgDiffCache[PATH_1]).toEqual(mockDiffInfo);
      });
    });

    it('does not update the cache when the path to update is already being updated.', () => {
      repo._updateDiffInfo([PATH_1]);
      // This second call should not kick off a second `hg diff` call, because
      // the first one should be still running.
      repo._updateDiffInfo([PATH_1]);
      expect(repo._service.fetchDiffInfo.calls.length).toBe(1);
    });

    it('removes paths that are marked for removal from the cache.', () => {
      // Set up some mock paths to be removed. One already exists in the cache,
      // the other is going to be attempted to be updated. Both should be removed.
      const testPathToRemove1 = PATH_1;
      const testPathToRemove2 = PATH_2;
      repo._hgDiffCache[testPathToRemove1] = {added: 0, deleted: 0, lineDiffs: []};
      repo._hgDiffCacheFilesToClear.add(testPathToRemove1);
      repo._hgDiffCacheFilesToClear.add(testPathToRemove2);

      waitsForPromise(async () => {
        await repo._updateDiffInfo([testPathToRemove2]);
        expect(repo._hgDiffCache[testPathToRemove1]).not.toBeDefined();
        expect(repo._hgDiffCache[testPathToRemove2]).not.toBeDefined();
      });
    });
  });

  describe('::getCachedPathStatus/::getPathStatus', () => {
    it('handles a null or undefined input "path" but handles paths with those names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.MODIFIED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.MODIFIED,
      };
      expect(repo.getCachedPathStatus(null)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(undefined)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(PATH_CALLED_NULL)).toBe(StatusCodeNumber.MODIFIED);
      expect(repo.getCachedPathStatus(PATH_CALLED_UNDEFINED)).toBe(StatusCodeNumber.MODIFIED);
    });
  });

  describe('::isStatusModified', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusModified(null)).toBe(false);
      expect(repo.isStatusModified(undefined)).toBe(false);
    });
  });

  describe('::isStatusNew', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusNew(null)).toBe(false);
      expect(repo.isStatusNew(undefined)).toBe(false);
    });
  });

  describe('::getDiffStats', () => {
    it('returns clean stats if the path is null or undefined, but handles paths with those'
      + ' names.', () => {
      const mockDiffInfo = {
        added: 1,
        deleted: 1,
        lineDiffs: [{
          oldStart: 2,
          oldLines: 1,
          newStart: 2,
          newLines: 1,
        }],
      };
      // Force the state of the cache.
      repo._hgDiffCache = {
        [PATH_CALLED_NULL]: mockDiffInfo,
        [PATH_CALLED_UNDEFINED]: mockDiffInfo,
      };
      const cleanStats = {added: 0, deleted: 0};
      const expectedChangeStats = {added: 1, deleted: 1};
      expect(repo.getDiffStats(null)).toEqual(cleanStats);
      expect(repo.getDiffStats(undefined)).toEqual(cleanStats);
      expect(repo.getDiffStats(PATH_CALLED_NULL)).toEqual(expectedChangeStats);
      expect(repo.getDiffStats(PATH_CALLED_UNDEFINED)).toEqual(expectedChangeStats);
    });
  });

  describe('::getLineDiffs', () => {
    it('returns an empty array if the path is null or undefined, but handles paths with those'
      + ' names.', () => {
      const mockDiffInfo = {
        added: 1,
        deleted: 1,
        lineDiffs: [{
          oldStart: 2,
          oldLines: 1,
          newStart: 2,
          newLines: 1,
        }],
      };
      // Force the state of the cache.
      repo._hgDiffCache = {
        [PATH_CALLED_NULL]: mockDiffInfo,
        [PATH_CALLED_UNDEFINED]: mockDiffInfo,
      };
      // For now the second argument, 'text', is not used.
      expect(repo.getLineDiffs(null, null)).toEqual([]);
      expect(repo.getLineDiffs(undefined, null)).toEqual([]);
      expect(repo.getLineDiffs(PATH_CALLED_NULL, null)).toEqual(mockDiffInfo.lineDiffs);
      expect(repo.getLineDiffs(PATH_CALLED_UNDEFINED, null)).toEqual(mockDiffInfo.lineDiffs);
    });
  });

  describe('::destroy', () => {
    it('should do cleanup without throwing an exception.', () => {
      const spy = jasmine.createSpy();
      repo.onDidDestroy(spy);
      repo.destroy();
      expect(spy).toHaveBeenCalled();
    });
  });
});
コード例 #19
0
describe('HgRepositoryClient', () => {
  const tempDir = temp.mkdirSync('testproj');
  const tempSubDir = temp.mkdirSync({dir: tempDir});

  const repoPath = nuclideUri.join(tempDir, '.hg');
  const workingDirectory = new Directory(tempDir);
  const projectDirectory = new Directory(tempSubDir);
  const repoOptions = {
    originURL: 'http://test.com/testproj',
    workingDirectory,
    projectRootDirectory: projectDirectory,
  };

  // Manufactures the absolute path of a file that should pass as being
  // within the repo.
  const createFilePath = filename => {
    return nuclideUri.join(projectDirectory.getPath(), filename);
  };

  // Some test "absolute" paths.
  const PATH_1 = createFilePath('test1.js');
  const PATH_2 = createFilePath('test2.js');
  const PATH_3 = createFilePath('test3.js');
  const PATH_4 = createFilePath('test4.js');
  const PATH_5 = createFilePath('test5.js');
  const PATH_6 = createFilePath('test6.js');
  const PATH_7 = createFilePath('test7.js');
  const PATH_CALLED_NULL = createFilePath('null');
  const PATH_CALLED_UNDEFINED = createFilePath('undefined');

  let mockHgService: HgServiceType = (null: any);
  let repo: HgRepositoryClient = (null: any);

  beforeEach(() => {
    mockHgService = ((new MockHgService(): any): HgServiceType);
    repo = new HgRepositoryClient(repoPath, mockHgService, repoOptions);
  });

  describe('::getType()', () => {
    it('returns "hg"', () => {
      expect(repo.getType()).toBe('hg');
    });
  });

  describe('::getProjectDirectory', () => {
    it(
      'returns the path of the root project folder in Atom that this Client provides information' +
        ' about.',
      () => {
        expect(repo.getProjectDirectory()).toBe(projectDirectory.getPath());
      },
    );
  });

  describe('::isPathIgnored', () => {
    it('returns true if the path is marked ignored in the cache.', () => {
      // Force the state of the cache.
      repo._sharedMembers.hgStatusCache = new Map([
        [PATH_1, StatusCodeNumber.IGNORED],
      ]);
      expect(repo.isPathIgnored(PATH_1)).toBe(true);
    });

    it('returns true if the path is, or is within, the .hg directory.', () => {
      expect(repo.isPathIgnored(repoPath)).toBe(true);
      expect(repo.isPathIgnored(nuclideUri.join(repoPath, 'blah'))).toBe(true);
    });

    it('returns false if the path is not in the cache and is not the .hg directory.', () => {
      expect(repo.isPathIgnored('/A/Random/Path')).toBe(false);
      const parsedPath = nuclideUri.parsePath(repoPath);
      expect(repo.isPathIgnored(parsedPath.root)).toBe(false);
      expect(repo.isPathIgnored(parsedPath.dir)).toBe(false);
    });

    it(
      'returns false if the path is null or undefined, but handles files with those' +
        ' names.',
      () => {
        // Force the state of the cache.
        repo._sharedMembers.hgStatusCache = new Map([
          [PATH_CALLED_NULL, StatusCodeNumber.IGNORED],
          [PATH_CALLED_UNDEFINED, StatusCodeNumber.IGNORED],
        ]);
        expect(repo.isPathIgnored(null)).toBe(false);
        expect(repo.isPathIgnored(undefined)).toBe(false);
        expect(repo.isPathIgnored(PATH_CALLED_NULL)).toBe(true);
        expect(repo.isPathIgnored(PATH_CALLED_UNDEFINED)).toBe(true);
      },
    );
  });

  describe('::isPathNew', () => {
    it(
      'returns false if the path is null or undefined, but handles files with those' +
        ' names.',
      () => {
        // Force the state of the cache.
        repo._sharedMembers.hgStatusCache = new Map([
          [PATH_CALLED_NULL, StatusCodeNumber.ADDED],
          [PATH_CALLED_UNDEFINED, StatusCodeNumber.ADDED],
        ]);
        expect(repo.isPathNew(null)).toBe(false);
        expect(repo.isPathNew(undefined)).toBe(false);
        expect(repo.isPathNew(PATH_CALLED_NULL)).toBe(true);
        expect(repo.isPathNew(PATH_CALLED_UNDEFINED)).toBe(true);
      },
    );
  });

  describe('::isPathModified', () => {
    it(
      'returns false if the path is null or undefined, but handles files with those' +
        ' names.',
      () => {
        // Force the state of the cache.
        repo._sharedMembers.hgStatusCache = new Map([
          [PATH_CALLED_NULL, StatusCodeNumber.MODIFIED],
          [PATH_CALLED_UNDEFINED, StatusCodeNumber.MODIFIED],
        ]);
        expect(repo.isPathModified(null)).toBe(false);
        expect(repo.isPathModified(undefined)).toBe(false);
        expect(repo.isPathModified(PATH_CALLED_NULL)).toBe(true);
        expect(repo.isPathModified(PATH_CALLED_UNDEFINED)).toBe(true);
      },
    );
  });

  describe('::isPathAdded', () => {
    it(
      'returns false if the path is null, untracked, modified or deleted' +
        ' names.',
      () => {
        // Force the state of the cache.
        repo._sharedMembers.hgStatusCache = new Map([
          [PATH_CALLED_NULL, StatusCodeNumber.ADDED],
          [PATH_CALLED_UNDEFINED, StatusCodeNumber.ADDED],
          [PATH_1, StatusCodeNumber.ADDED],
          [PATH_2, StatusCodeNumber.CLEAN],
          [PATH_3, StatusCodeNumber.IGNORED],
          [PATH_4, StatusCodeNumber.MISSING],
          [PATH_5, StatusCodeNumber.MODIFIED],
          [PATH_6, StatusCodeNumber.REMOVED],
          [PATH_7, StatusCodeNumber.UNTRACKED],
        ]);
        expect(repo.isPathAdded(null)).toBe(false);
        expect(repo.isPathAdded(undefined)).toBe(false);
        expect(repo.isPathAdded(PATH_CALLED_NULL)).toBe(true);
        expect(repo.isPathAdded(PATH_CALLED_UNDEFINED)).toBe(true);
        expect(repo.isPathAdded(PATH_1)).toBe(true);
        expect(repo.isPathAdded(PATH_2)).toBe(false);
        expect(repo.isPathAdded(PATH_3)).toBe(false);
        expect(repo.isPathAdded(PATH_4)).toBe(false);
        expect(repo.isPathAdded(PATH_5)).toBe(false);
        expect(repo.isPathAdded(PATH_6)).toBe(false);
        expect(repo.isPathAdded(PATH_7)).toBe(false);
      },
    );
  });

  describe('::isPathUntracked', () => {
    it(
      'returns false if the path is null, untracked, modified or deleted' +
        ' names.',
      () => {
        // Force the state of the cache.
        repo._sharedMembers.hgStatusCache = new Map([
          [PATH_CALLED_NULL, StatusCodeNumber.UNTRACKED],
          [PATH_CALLED_UNDEFINED, StatusCodeNumber.UNTRACKED],
          [PATH_1, StatusCodeNumber.UNTRACKED],
          [PATH_2, StatusCodeNumber.CLEAN],
          [PATH_3, StatusCodeNumber.IGNORED],
          [PATH_4, StatusCodeNumber.MISSING],
          [PATH_5, StatusCodeNumber.MODIFIED],
          [PATH_6, StatusCodeNumber.REMOVED],
          [PATH_7, StatusCodeNumber.ADDED],
        ]);
        expect(repo.isPathUntracked(null)).toBe(false);
        expect(repo.isPathUntracked(undefined)).toBe(false);
        expect(repo.isPathUntracked(PATH_CALLED_NULL)).toBe(true);
        expect(repo.isPathUntracked(PATH_CALLED_UNDEFINED)).toBe(true);
        expect(repo.isPathUntracked(PATH_1)).toBe(true);
        expect(repo.isPathUntracked(PATH_2)).toBe(false);
        expect(repo.isPathUntracked(PATH_3)).toBe(false);
        expect(repo.isPathUntracked(PATH_4)).toBe(false);
        expect(repo.isPathUntracked(PATH_5)).toBe(false);
        expect(repo.isPathUntracked(PATH_6)).toBe(false);
        expect(repo.isPathUntracked(PATH_7)).toBe(false);
      },
    );
  });

  describe('::getCachedPathStatus', () => {
    beforeEach(() => {
      repo._sharedMembers.hgStatusCache = new Map([
        [PATH_1, StatusCodeNumber.MODIFIED],
        [PATH_2, StatusCodeNumber.IGNORED],
      ]);
    });

    it('retrieves cached hg status.', () => {
      // Force the state of the cache.
      const status = repo.getCachedPathStatus(PATH_1);
      expect(repo.isStatusModified(status)).toBe(true);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('retrieves cached hg ignore status.', () => {
      const status = repo.getCachedPathStatus(PATH_2);
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('returns a clean status by default.', () => {
      const status = repo.getCachedPathStatus('path-not-in-cache');
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });
  });

  describe('the hgDiffCache', () => {
    beforeEach(() => {
      // Unfortunately, when the temp files in these tests are opened in the editor,
      // editor.getPath() returns the original file path with '/private/' appended
      // to it. Thus, the path returned from editor.getPath() (which is what is
      // used in HgRepository) would fail a real 'contains' method. So we override
      // this to the expected path.
      featureConfig.set('nuclide-hg-repository.enableDiffStats', true);
      const workingDirectoryClone = new Directory(tempDir);
      spyOn(workingDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return workingDirectoryClone.contains(prefixRemovedPath);
        }
        return workingDirectoryClone.contains(filePath);
      });

      const projectDirectoryClone = new Directory(tempSubDir);
      spyOn(projectDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return projectDirectoryClone.contains(prefixRemovedPath);
        }
        return projectDirectoryClone.contains(filePath);
      });

      spyOn(repo, '_updateDiffInfo').andReturn(Observable.of('fake'));
      spyOn(repo, '_observePaneItemVisibility').andReturn(Observable.of(true));
    });
  });

  describe('::_updateDiffInfo', () => {
    const mockDiffInfo = {
      added: 2,
      deleted: 11,
      lineDiffs: [
        {
          oldStart: 150,
          oldLines: 11,
          newStart: 150,
          newLines: 2,
        },
      ],
    };

    beforeEach(() => {
      spyOn(repo, '_getCurrentHeadId').andReturn(Observable.of('test'));
      spyOn(
        repo._sharedMembers.service,
        'fetchFileContentAtRevision',
      ).andCallFake(filePath => {
        return new Observable.of('test').publish();
      });
      spyOn(repo, '_getFileDiffs').andCallFake(pathsToFetch => {
        const diffs = [];
        for (const filePath of pathsToFetch) {
          diffs.push([filePath, mockDiffInfo]);
        }
        return Observable.of(diffs);
      });
      spyOn(workingDirectory, 'contains').andCallFake(() => {
        return true;
      });
    });
  });

  describe('::getCachedPathStatus/::getPathStatus', () => {
    it('handles a null or undefined input "path" but handles paths with those names.', () => {
      // Force the state of the cache.
      repo._sharedMembers.hgStatusCache = new Map([
        [PATH_CALLED_NULL, StatusCodeNumber.MODIFIED],
        [PATH_CALLED_UNDEFINED, StatusCodeNumber.MODIFIED],
      ]);
      expect(repo.getCachedPathStatus(null)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(undefined)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(PATH_CALLED_NULL)).toBe(
        StatusCodeNumber.MODIFIED,
      );
      expect(repo.getCachedPathStatus(PATH_CALLED_UNDEFINED)).toBe(
        StatusCodeNumber.MODIFIED,
      );
    });
  });

  describe('::isStatusModified', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusModified(null)).toBe(false);
      expect(repo.isStatusModified(undefined)).toBe(false);
    });
  });

  describe('::isStatusNew', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusNew(null)).toBe(false);
      expect(repo.isStatusNew(undefined)).toBe(false);
    });
  });

  describe('::destroy', () => {
    it('should do cleanup without throwing an exception.', () => {
      const spy = jasmine.createSpy();
      repo.onDidDestroy(spy);
      repo.destroy();
      expect(spy).toHaveBeenCalled();
    });
  });
});
コード例 #20
0
describe('HgRepositoryClient', () => {
  const tempDir = temp.mkdirSync('testproj');
  const tempSubDir = temp.mkdirSync({dir: tempDir});

  const repoPath = path.join(tempDir, '.hg');
  const workingDirectory = new Directory(tempDir);
  const projectDirectory = new Directory(tempSubDir);
  const repoOptions = {
    originURL: 'http://test.com/testproj',
    workingDirectory,
    projectRootDirectory: projectDirectory,
  };

  // Manufactures the absolute path of a file that should pass as being
  // within the repo.
  const createFilePath = filename => {
    return path.join(projectDirectory.getPath(), filename);
  };

  // Some test "absolute" paths.
  const PATH_1 = createFilePath('test1.js');
  const PATH_2 = createFilePath('test2.js');
  const PATH_3 = createFilePath('test3.js');
  const PATH_4 = createFilePath('test4.js');
  const PATH_5 = createFilePath('test5.js');
  const PATH_6 = createFilePath('test6.js');
  const PATH_7 = createFilePath('test7.js');
  const PATH_CALLED_NULL = createFilePath('null');
  const PATH_CALLED_UNDEFINED = createFilePath('undefined');

  let mockHgService: HgServiceType = (null: any);
  let repo: HgRepositoryClient = (null: any);

  beforeEach(() => {
    mockHgService = ((new MockHgService(): any): HgServiceType);
    repo = new HgRepositoryClient(repoPath, mockHgService, repoOptions);
  });

  describe('::getType()', () => {
    it('returns "hg"', () => {
      expect(repo.getType()).toBe('hg');
    });
  });

  describe('::getProjectDirectory', () => {
    it('returns the path of the root project folder in Atom that this Client provides information'
    + ' about.', () => {
      expect(repo.getProjectDirectory()).toBe(projectDirectory.getPath());
    });
  });

  describe('::getStatuses', () => {
    beforeEach(() => {
      // Test setup: Mock out the dependency on HgRepository::_updateStatuses, and set up the cache
      // state.
      const mockFetchedStatuses = {[PATH_1]: StatusCodeId.ADDED};
      spyOn(repo, '_updateStatuses').andCallFake((paths, options) => {
        const statuses = new Map();
        paths.forEach(filePath => {
          statuses.set(filePath, mockFetchedStatuses[filePath]);
        });
        return Promise.resolve(statuses);
      });
      repo._hgStatusCache = {
        [PATH_2]: StatusCodeId.IGNORED,
        [PATH_3]: StatusCodeId.MODIFIED,
      };
    });

    it('returns statuses from the cache when possible, and only fetches the status for cache'
      + ' misses.', () => {
      const hgStatusOptions = {hgStatusOption: HgStatusOption.ALL_STATUSES};
      waitsForPromise(async () => {
        const statusMap = await repo.getStatuses([PATH_1, PATH_2], hgStatusOptions);
        expect(repo._updateStatuses).toHaveBeenCalledWith([PATH_1], hgStatusOptions);
        expect(statusMap).toEqual(new Map([
          [PATH_1, StatusCodeNumber.ADDED],
          [PATH_2, StatusCodeNumber.IGNORED],
        ]));
      });
    });

    it('when reading from the cache, it respects the hgStatusOption.', () => {
      waitsForPromise(async () => {
        const statusMap = await repo.getStatuses(
            [PATH_2, PATH_3], {hgStatusOption: HgStatusOption.ONLY_NON_IGNORED});
        expect(repo._updateStatuses).not.toHaveBeenCalled();
        expect(statusMap).toEqual(new Map([
          [PATH_3, StatusCodeNumber.MODIFIED],
        ]));
      });

      waitsForPromise(async () => {
        const statusMap = await repo.getStatuses(
            [PATH_2, PATH_3], {hgStatusOption: HgStatusOption.ONLY_IGNORED});
        expect(repo._updateStatuses).not.toHaveBeenCalled();
        expect(statusMap).toEqual(new Map([
          [PATH_2, StatusCodeNumber.IGNORED],
        ]));
      });
    });
  });

  describe('::_updateStatuses', () => {
    const nonIgnoredOption = {hgStatusOption: HgStatusOption.ONLY_NON_IGNORED};
    const onlyIgnoredOption = {hgStatusOption: HgStatusOption.ONLY_IGNORED};
    const mockHgStatusFetchData = new Map([
      [PATH_1, StatusCodeId.ADDED],
      [PATH_2, StatusCodeId.UNTRACKED],
      [PATH_3, StatusCodeId.CLEAN],
      [PATH_6, StatusCodeId.CLEAN],
      [PATH_7, StatusCodeId.MODIFIED],
    ]);
    let mockOldCacheState;

    beforeEach(() => {
      mockOldCacheState = {
        [PATH_1]: StatusCodeId.IGNORED,
        [PATH_2]: StatusCodeId.UNTRACKED,
        [PATH_3]: StatusCodeId.MODIFIED,
        [PATH_4]: StatusCodeId.IGNORED,
        [PATH_5]: StatusCodeId.MODIFIED,
      };

      spyOn(repo._service, 'fetchStatuses').andCallFake((paths, options) => {
        const statusMap = new Map();
        paths.forEach(filePath => {
          const fetchedStatus = mockHgStatusFetchData.get(filePath);
          if (fetchedStatus) {
            statusMap.set(filePath, fetchedStatus);
          }
        });
        return Promise.resolve(statusMap);
      });
      repo._hgStatusCache = {};
      Object.keys(mockOldCacheState).forEach(filePath => {
        repo._hgStatusCache[filePath] = mockOldCacheState[filePath];
      });
      // Make it so all of the test paths are deemed within the repo.
      spyOn(workingDirectory, 'contains').andCallFake(() => {
        return true;
      });
    });

    it('does a fresh fetch for the hg status for all paths it is passed, and returns them.', () => {
      const paths = [PATH_1, PATH_2];
      waitsForPromise(async () => {
        const output = await repo._updateStatuses(paths, nonIgnoredOption);
        expect(repo._service.fetchStatuses).toHaveBeenCalledWith(paths, nonIgnoredOption);
        const expectedStatus = new Map([
          [PATH_1, mockHgStatusFetchData.get(PATH_1)],
          [PATH_2, mockHgStatusFetchData.get(PATH_2)],
        ]);
        expect(output).toEqual(expectedStatus);
      });
    });

    describe('it removes a path from the cache if the fetch indicates its status is unknown but'
      + ' incorrect in the cache, as informed by the hgStatusOption passed in', () => {
      const pathsWithNoStatusReturned = [PATH_4, PATH_5];

      it('Case 1: HgStatusOption.ONLY_NON_IGNORED', () => {
        waitsForPromise(async () => {
          await repo._updateStatuses(pathsWithNoStatusReturned, nonIgnoredOption);
          // PATH_4 was queried for but not returned, but the fetch was for non-ignored files.
          //   We have no evidence that its status is out of date, so it should remain 'ignored' in
          //   the cache.
          // PATH_5 was queried for but not returned, and the fetch was for non-ignored files.
          //   This means its state is no longer 'modified', as it was listed in the cache.
          expect(repo._hgStatusCache[PATH_4]).toBe(StatusCodeId.IGNORED);
          expect(repo._hgStatusCache[PATH_5]).toBeUndefined();
        });
      });

      it('Case 2: HgStatusOption.ONLY_IGNORED', () => {
        waitsForPromise(async () => {
          await repo._updateStatuses(pathsWithNoStatusReturned, onlyIgnoredOption);
          // PATH_5 was queried for but not returned, but the fetch was for ignored files.
          //   We have no evidence that its status is out of date, so it should remain 'modified' in
          //   the cache.
          // PATH_4 was queried for but not returned, and the fetch was for ignored files.
          //   This means its state is no longer 'ignored', as it was listed in the cache.
          expect(repo._hgStatusCache[PATH_5]).toBe(StatusCodeId.MODIFIED);
          expect(repo._hgStatusCache[PATH_4]).toBeUndefined();
        });
      });
    });

    it('does not add "clean" files to the cache and removes them if they are in the cache.', () => {
      const pathsWithCleanStatusReturned = [PATH_3, PATH_6];
      waitsForPromise(async () => {
        await repo._updateStatuses(
          pathsWithCleanStatusReturned, {hgStatusOption: HgStatusOption.ALL_STATUSES});
        // PATH_3 was previously in the cache. PATH_6 was never in the cache.
        expect(repo._hgStatusCache[PATH_3]).toBeUndefined();
        expect(repo._hgStatusCache[PATH_6]).toBeUndefined();
      });
    });

    it('triggers the callbacks registered through ::onDidChangeStatuses and'
      + ' ::onDidChangeStatus.', () => {
      const callbackSpyForStatuses = jasmine.createSpy('::onDidChangeStatuses spy');
      repo.onDidChangeStatuses(callbackSpyForStatuses);
      const callbackSpyForStatus = jasmine.createSpy('::onDidChangeStatus spy');
      repo.onDidChangeStatus(callbackSpyForStatus);

      // File existed in the cache, and its status changed.
      const expectedChangeEvent1 = {
        path: PATH_1,
        pathStatus: StatusCodeNumber.ADDED,
      };

      // File did not exist in the cache, and its status is modified.
      const expectedChangeEvent2 = {
        path: PATH_7,
        pathStatus: StatusCodeNumber.MODIFIED,
      };

      waitsForPromise(async () => {
        // We must pass in the updated filenames to catch the case when a cached status turns to
        // 'clean'.
        await repo._updateStatuses(
          [PATH_1, PATH_2, PATH_6, PATH_7], {hgStatusOption: HgStatusOption.ALL_STATUSES});
        expect(callbackSpyForStatuses.calls.length).toBe(1);
        expect(callbackSpyForStatus.calls.length).toBe(2);
        // PATH_2 existed in the cache, and its status did not change, so it shouldn't generate an
        // event.
        // PATH_6 did not exist in the cache, but its status is clean, so it shouldn't generate an
        // event.
        expect(callbackSpyForStatus).toHaveBeenCalledWith(expectedChangeEvent1);
        expect(callbackSpyForStatus).toHaveBeenCalledWith(expectedChangeEvent2);
      });
    });
  });

  describe('::_updateChangedPaths', () => {
    it(
      'triggers a full refresh of the state of the Hg statuses if there are more than ' +
      'MAX_INDIVIDUAL_CHANGED_PATHS paths changed within the project directory.',
      () => {
        const mockUpdate = [PATH_1, PATH_2];
        // This test is only valid if the number of relevant files in the update
        // > MAX_INDIVIDUAL_CHANGED_PATHS. If MAX_INDIVIDUAL_CHANGED_PATHS changes,
        // this test needs to be updated.
        expect(mockUpdate.length).toBeGreaterThan(MAX_INDIVIDUAL_CHANGED_PATHS);
        spyOn(repo, '_serializedRefreshStatusesCache');

        waitsForPromise(async () => {
          await repo._updateChangedPaths(mockUpdate);
          expect(repo._serializedRefreshStatusesCache).toHaveBeenCalled();
        });
      }
    );

    it(
      'triggers an update for the state of the Hg statuses of individual files if there ' +
      'are <= MAX_INDIVIDUAL_CHANGED_PATHS paths changed within the project directory.',
      () => {
        const mockUpdate = [PATH_1];
        // This test is only valid if the number of relevant files in the update
        // <= MAX_INDIVIDUAL_CHANGED_PATHS. If MAX_INDIVIDUAL_CHANGED_PATHS changes,
        // this test needs to be updated.
        expect(mockUpdate.length).not.toBeGreaterThan(MAX_INDIVIDUAL_CHANGED_PATHS);
        spyOn(repo, '_updateStatuses');

        waitsForPromise(async () => {
          await repo._updateChangedPaths(mockUpdate);
          expect(repo._updateStatuses).toHaveBeenCalledWith(
            [PATH_1],
            {hgStatusOption: HgStatusOption.ALL_STATUSES},
          );
          expect(repo._updateStatuses).not.toHaveBeenCalledWith(
            [repo.getProjectDirectory()],
            {hgStatusOption: HgStatusOption.ONLY_NON_IGNORED},
          );
        });
      }
    );

    it(
      'does not triggers a full refresh of the state of the Hg statuses if none of ' +
      'the changed paths are within the project directory.', () => {
      const path_not_in_project = '/Random/Path';
      const mockUpdate = [path_not_in_project];
      spyOn(repo, '_updateStatuses');

      waitsForPromise(async () => {
        await repo._updateChangedPaths(mockUpdate);
        expect(repo._updateStatuses).not.toHaveBeenCalledWith(
          [repo.getProjectDirectory()],
          {hgStatusOption: HgStatusOption.ONLY_NON_IGNORED},
        );
      });
    }
    );
  });

  describe('_refreshStatusesOfAllFilesInCache', () => {
    it('refreshes the status of all paths currently in the cache', () => {
      // Test setup: force the state of the repo.
      const testRepoState = {
        [PATH_1]: StatusCodeId.IGNORED,
        [PATH_2]: StatusCodeId.MODIFIED,
        [PATH_3]: StatusCodeId.ADDED,
      };
      repo._hgStatusCache = testRepoState;
      spyOn(repo, '_updateStatuses').andCallFake((filePaths, options) => {
        // The cache should be cleared before being fully refreshed.
        expect(repo._hgStatusCache).toEqual({});
      });

      waitsForPromise(async () => {
        await repo._refreshStatusesOfAllFilesInCache();
        expect(repo._updateStatuses).toHaveBeenCalledWith(
          [repo.getProjectDirectory()],
          {hgStatusOption: HgStatusOption.ONLY_NON_IGNORED},
        );
      });
    });
  });

  describe('::isPathIgnored', () => {
    it('returns true if the path is marked ignored in the cache.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_1]: StatusCodeId.IGNORED,
      };
      expect(repo.isPathIgnored(PATH_1)).toBe(true);
    });

    it('returns true if the path is, or is within, the .hg directory.', () => {
      expect(repo.isPathIgnored(repoPath)).toBe(true);
      expect(repo.isPathIgnored(path.join(repoPath, 'blah'))).toBe(true);
    });

    it('returns false if the path is not in the cache and is not the .hg directory.', () => {
      expect(repo.isPathIgnored('/A/Random/Path')).toBe(false);
      const parsedPath = path.parse(repoPath);
      expect(repo.isPathIgnored(parsedPath.root)).toBe(false);
      expect(repo.isPathIgnored(parsedPath.dir)).toBe(false);
    });

    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.IGNORED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.IGNORED,
      };
      expect(repo.isPathIgnored(null)).toBe(false);
      expect(repo.isPathIgnored(undefined)).toBe(false);
      expect(repo.isPathIgnored(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathIgnored(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathNew', () => {
    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.ADDED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.ADDED,
      };
      expect(repo.isPathNew(null)).toBe(false);
      expect(repo.isPathNew(undefined)).toBe(false);
      expect(repo.isPathNew(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathNew(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathModified', () => {
    it('returns false if the path is null or undefined, but handles files with those'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.MODIFIED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.MODIFIED,
      };
      expect(repo.isPathModified(null)).toBe(false);
      expect(repo.isPathModified(undefined)).toBe(false);
      expect(repo.isPathModified(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathModified(PATH_CALLED_UNDEFINED)).toBe(true);
    });
  });

  describe('::isPathAdded', () => {
    it('returns false if the path is null, untracked, modified or deleted'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.ADDED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.ADDED,
        [PATH_1]: StatusCodeId.ADDED,
        [PATH_2]: StatusCodeId.CLEAN,
        [PATH_3]: StatusCodeId.IGNORED,
        [PATH_4]: StatusCodeId.MISSING,
        [PATH_5]: StatusCodeId.MODIFIED,
        [PATH_6]: StatusCodeId.REMOVED,
        [PATH_7]: StatusCodeId.UNTRACKED,
      };
      expect(repo.isPathAdded(null)).toBe(false);
      expect(repo.isPathAdded(undefined)).toBe(false);
      expect(repo.isPathAdded(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathAdded(PATH_CALLED_UNDEFINED)).toBe(true);
      expect(repo.isPathAdded(PATH_1)).toBe(true);
      expect(repo.isPathAdded(PATH_2)).toBe(false);
      expect(repo.isPathAdded(PATH_3)).toBe(false);
      expect(repo.isPathAdded(PATH_4)).toBe(false);
      expect(repo.isPathAdded(PATH_5)).toBe(false);
      expect(repo.isPathAdded(PATH_6)).toBe(false);
      expect(repo.isPathAdded(PATH_7)).toBe(false);
    });
  });

  describe('::isPathUntracked', () => {
    it('returns false if the path is null, untracked, modified or deleted'
      + ' names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.UNTRACKED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.UNTRACKED,
        [PATH_1]: StatusCodeId.UNTRACKED,
        [PATH_2]: StatusCodeId.CLEAN,
        [PATH_3]: StatusCodeId.IGNORED,
        [PATH_4]: StatusCodeId.MISSING,
        [PATH_5]: StatusCodeId.MODIFIED,
        [PATH_6]: StatusCodeId.REMOVED,
        [PATH_7]: StatusCodeId.ADDED,
      };
      expect(repo.isPathUntracked(null)).toBe(false);
      expect(repo.isPathUntracked(undefined)).toBe(false);
      expect(repo.isPathUntracked(PATH_CALLED_NULL)).toBe(true);
      expect(repo.isPathUntracked(PATH_CALLED_UNDEFINED)).toBe(true);
      expect(repo.isPathUntracked(PATH_1)).toBe(true);
      expect(repo.isPathUntracked(PATH_2)).toBe(false);
      expect(repo.isPathUntracked(PATH_3)).toBe(false);
      expect(repo.isPathUntracked(PATH_4)).toBe(false);
      expect(repo.isPathUntracked(PATH_5)).toBe(false);
      expect(repo.isPathUntracked(PATH_6)).toBe(false);
      expect(repo.isPathUntracked(PATH_7)).toBe(false);
    });
  });

  describe('::getCachedPathStatus', () => {
    beforeEach(() => {
      repo._hgStatusCache = {
        [PATH_1]: StatusCodeId.MODIFIED,
        [PATH_2]: StatusCodeId.IGNORED,
      };
    });

    it('retrieves cached hg status.', () => {
      // Force the state of the cache.
      const status = repo.getCachedPathStatus(PATH_1);
      expect(repo.isStatusModified(status)).toBe(true);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('retrieves cached hg ignore status.', () => {
      const status = repo.getCachedPathStatus(PATH_2);
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });

    it('returns a clean status by default.', () => {
      const status = repo.getCachedPathStatus('path-not-in-cache');
      // The status codes have no meaning; just test the expected translated
      // meanings.
      expect(repo.isStatusModified(status)).toBe(false);
      expect(repo.isStatusNew(status)).toBe(false);
    });
  });

  describe('the hgDiffCache', () => {
    beforeEach(() => {
      // Unfortunately, when the temp files in these tests are opened in the editor,
      // editor.getPath() returns the original file path with '/private/' appended
      // to it. Thus, the path returned from editor.getPath() (which is what is
      // used in HgRepository) would fail a real 'contains' method. So we override
      // this to the expected path.
      const workingDirectoryClone = new Directory(tempDir);
      spyOn(workingDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return workingDirectoryClone.contains(prefixRemovedPath);
        }
        return workingDirectoryClone.contains(filePath);
      });

      const projectDirectoryClone = new Directory(tempSubDir);
      spyOn(projectDirectory, 'contains').andCallFake(filePath => {
        const prefix = '/private';
        if (filePath.startsWith(prefix)) {
          const prefixRemovedPath = filePath.slice(prefix.length);
          return projectDirectoryClone.contains(prefixRemovedPath);
        }
        return projectDirectoryClone.contains(filePath);
      });
    });

    xit('is updated when the active pane item changes to an editor, if the editor file is in the'
      + ' project.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync({dir: projectDirectory.getPath()});
      waitsForPromise(async () => {
        const editor = await atom.workspace.open(file.path);
        expect(repo._updateDiffInfo.calls.length).toBe(1);
        expect(repo._updateDiffInfo).toHaveBeenCalledWith(editor.getPath());
      });
    });

    it('is not updated when the active pane item changes to an editor whose file is not in the'
      + ' repo.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync();
      waitsForPromise(async () => {
        await atom.workspace.open(file.path);
        expect(repo._updateDiffInfo.calls.length).toBe(0);
      });
    });

    it('marks a file to be removed from the cache after its editor is closed, if the file is in the'
      + ' project.', () => {
      spyOn(repo, '_updateDiffInfo');
      const file = temp.openSync({dir: projectDirectory.getPath()});
      waitsForPromise(async () => {
        const editor = await atom.workspace.open(file.path);
        expect(repo._hgDiffCacheFilesToClear.size).toBe(0);
        editor.destroy();
        const expectedSet = new Set([editor.getPath()]);
        expect(repo._hgDiffCacheFilesToClear).toEqual(expectedSet);
      });
    });
  });

  describe('::_updateDiffInfo', () => {
    const mockDiffInfo = {
      added: 2,
      deleted: 11,
      lineDiffs: [{
        oldStart: 150,
        oldLines: 11,
        newStart: 150,
        newLines: 2,
      }],
    };

    beforeEach(() => {
      spyOn(repo._service, 'fetchDiffInfo').andCallFake(filePaths => {
        const mockFetchedPathToDiffInfo = new Map();
        for (const filePath of filePaths) {
          mockFetchedPathToDiffInfo.set(filePath, mockDiffInfo);
        }
        return Promise.resolve(mockFetchedPathToDiffInfo);
      });
      spyOn(workingDirectory, 'contains').andCallFake(() => {
        return true;
      });
    });

    it('updates the cache when the path to update is not already being updated.', () => {
      waitsForPromise(async () => {
        expect(repo._hgDiffCache[PATH_1]).toBeUndefined();
        await repo._updateDiffInfo([PATH_1]);
        expect(repo._hgDiffCache[PATH_1]).toEqual(mockDiffInfo);
      });
    });

    it('does not update the cache when the path to update is already being updated.', () => {
      waitsForPromise(async () => {
        repo._updateDiffInfo([PATH_1]);
        // This second call should not kick off a second `hg diff` call, because
        // the first one should be still running.
        repo._updateDiffInfo([PATH_1]);
        expect(repo._service.fetchDiffInfo.calls.length).toBe(1);
      });
    });

    it('removes paths that are marked for removal from the cache.', () => {
      // Set up some mock paths to be removed. One already exists in the cache,
      // the other is going to be attempted to be updated. Both should be removed.
      const testPathToRemove1 = PATH_1;
      const testPathToRemove2 = PATH_2;
      repo._hgDiffCache[testPathToRemove1] = {added: 0, deleted: 0, lineDiffs: []};
      repo._hgDiffCacheFilesToClear.add(testPathToRemove1);
      repo._hgDiffCacheFilesToClear.add(testPathToRemove2);

      waitsForPromise(async () => {
        await repo._updateDiffInfo([testPathToRemove2]);
        expect(repo._hgDiffCache[testPathToRemove1]).not.toBeDefined();
        expect(repo._hgDiffCache[testPathToRemove2]).not.toBeDefined();
      });
    });
  });

  describe('::getDirectoryStatus', () => {
    const testDir = createFilePath('subDirectory');
    const subDirectory = path.join(testDir, 'dir1');
    const subSubDirectory = path.join(subDirectory, 'dir2');

    it('marks a directory as modified only if it is in the modified directories cache.', () => {
      // Force the state of the hgStatusCache.
      repo._modifiedDirectoryCache = new Map();
      repo._modifiedDirectoryCache.set(fsSync.ensureTrailingSeparator(testDir), 1);
      repo._modifiedDirectoryCache.set(fsSync.ensureTrailingSeparator(subDirectory), 1);

      expect(repo.getDirectoryStatus(testDir)).toBe(StatusCodeNumber.MODIFIED);
      expect(repo.getDirectoryStatus(subDirectory)).toBe(StatusCodeNumber.MODIFIED);
      expect(repo.getDirectoryStatus(subSubDirectory)).toBe(StatusCodeNumber.CLEAN);
    });

    it('handles a null or undefined input "path" but handles paths with those names.', () => {
      const dir_called_null = createFilePath('null');
      const dir_called_undefined = createFilePath('undefined');

      // Force the state of the cache.
      repo._modifiedDirectoryCache = new Map();
      repo._modifiedDirectoryCache.set(fsSync.ensureTrailingSeparator(dir_called_null), 1);
      repo._modifiedDirectoryCache.set(fsSync.ensureTrailingSeparator(dir_called_undefined), 1);

      expect(repo.getDirectoryStatus(null)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getDirectoryStatus(undefined)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getDirectoryStatus(dir_called_null)).toBe(StatusCodeNumber.MODIFIED);
      expect(repo.getDirectoryStatus(dir_called_undefined)).toBe(StatusCodeNumber.MODIFIED);
    });
  });

  describe('::getCachedPathStatus/::getPathStatus', () => {
    it('handles a null or undefined input "path" but handles paths with those names.', () => {
      // Force the state of the cache.
      repo._hgStatusCache = {
        [PATH_CALLED_NULL]: StatusCodeId.MODIFIED,
        [PATH_CALLED_UNDEFINED]: StatusCodeId.MODIFIED,
      };
      expect(repo.getCachedPathStatus(null)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(undefined)).toBe(StatusCodeNumber.CLEAN);
      expect(repo.getCachedPathStatus(PATH_CALLED_NULL)).toBe(StatusCodeNumber.MODIFIED);
      expect(repo.getCachedPathStatus(PATH_CALLED_UNDEFINED)).toBe(StatusCodeNumber.MODIFIED);
    });
  });

  describe('::isStatusModified', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusModified(null)).toBe(false);
      expect(repo.isStatusModified(undefined)).toBe(false);
    });
  });

  describe('::isStatusNew', () => {
    it('returns false for a null or undefined input.', () => {
      expect(repo.isStatusNew(null)).toBe(false);
      expect(repo.isStatusNew(undefined)).toBe(false);
    });
  });

  describe('::getDiffStats', () => {
    it('returns clean stats if the path is null or undefined, but handles paths with those'
      + ' names.', () => {
      const mockDiffInfo = {
        added: 1,
        deleted: 1,
        lineDiffs: [{
          oldStart: 2,
          oldLines: 1,
          newStart: 2,
          newLines: 1,
        }],
      };
      // Force the state of the cache.
      repo._hgDiffCache = {
        [PATH_CALLED_NULL]: mockDiffInfo,
        [PATH_CALLED_UNDEFINED]: mockDiffInfo,
      };
      const cleanStats = {added: 0, deleted: 0};
      const expectedChangeStats = {added: 1, deleted: 1};
      expect(repo.getDiffStats(null)).toEqual(cleanStats);
      expect(repo.getDiffStats(undefined)).toEqual(cleanStats);
      expect(repo.getDiffStats(PATH_CALLED_NULL)).toEqual(expectedChangeStats);
      expect(repo.getDiffStats(PATH_CALLED_UNDEFINED)).toEqual(expectedChangeStats);
    });
  });

  describe('::getLineDiffs', () => {
    it('returns an empty array if the path is null or undefined, but handles paths with those'
      + ' names.', () => {
      const mockDiffInfo = {
        added: 1,
        deleted: 1,
        lineDiffs: [{
          oldStart: 2,
          oldLines: 1,
          newStart: 2,
          newLines: 1,
        }],
      };
      // Force the state of the cache.
      repo._hgDiffCache = {
        [PATH_CALLED_NULL]: mockDiffInfo,
        [PATH_CALLED_UNDEFINED]: mockDiffInfo,
      };
      // For now the second argument, 'text', is not used.
      expect(repo.getLineDiffs(null, null)).toEqual([]);
      expect(repo.getLineDiffs(undefined, null)).toEqual([]);
      expect(repo.getLineDiffs(PATH_CALLED_NULL, null)).toEqual(mockDiffInfo.lineDiffs);
      expect(repo.getLineDiffs(PATH_CALLED_UNDEFINED, null)).toEqual(mockDiffInfo.lineDiffs);
    });
  });

  describe('::destroy', () => {
    it('should do cleanup without throwing an exception.', () => {
      const spy = jasmine.createSpy();
      repo.onDidDestroy(spy);
      repo.destroy();
      expect(spy).toHaveBeenCalled();
    });
  });

});