def get_staged_files_paths(self): """ Gets the relative paths of all staged files. Returns: A list with the relative paths of all files that are currently staged. """ head_hash = self.get_head_hash() with exec_in_dir(self.root): pipe = Popen(["git", "diff", "--cached", "--name-only", head_hash], stdout=PIPE, stderr=PIPE) out, err = pipe.communicate() staged_files = ( out.decode('utf-8')[:-1] # remove trailing '\n' .split('\n')) # drop nulls produce by split, if any staged_files = [file for file in staged_files if file] staged_files_paths = [ path.relpath(file, self.root) for file in staged_files ] return staged_files_paths
def __init__(self, path=None, *args, **kwargs): """ Args: path: an absolute path to the root or a subdirectory of a git repository. """ # get repository root with exec_in_dir(path): self.root = self._get_git_root()
def test_no_dir_change(self): # write a test file in current directory and check its path try: # test with exec_in_dir(None): test_file_path = path.join(self.cwd, "foo.txt") with open(test_file_path, "w") as foo: foo.write("a-ha!") self.assertEqual(path.abspath(getcwd()), path.abspath(self.cwd)) self.assertTrue(path.isfile(path.abspath(test_file_path))) except Exception: raise finally: # clean up remove(path.abspath(test_file_path))
def get_staged_file_content(self, staged_file_path): """ Gets the contents of a given staged file. Args: staged_file_path: the path of a file that is currently staged. Returns: A byte literal corresponding to the contents of the staged file. """ # quote the staged_file name or path in order to take care of # escaping in the shell with exec_in_dir(self.root): pipe = Popen( ["git", "show", ":%s" % staged_file_path], stdout=PIPE, stderr=PIPE) out, err = pipe.communicate() return out
def get_head_hash(self): """ Gets the current HEAD's hash. Returns: A string with the current HEAD's hash. """ with exec_in_dir(self.root): pipe = Popen(["git", "rev-parse", "--verify", "HEAD"], stdout=PIPE, stderr=PIPE) out, err = pipe.communicate() # strip the trailing '\n' head_hash = out.decode('utf-8')[:-1] # use the special hash if there is no HEAD if not head_hash: head_hash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" return head_hash
def test_dir_change(self): # write a test file in a subdirectory and check its path try: # make subdirectory subdir = path.join(self.cwd, "foo_subdir") mkdir(subdir) # test with exec_in_dir(subdir): test_file_path = path.join(getcwd(), "foo_sub.txt") with open(test_file_path, "w") as foo: foo.write("a-ha!") self.assertEqual(path.abspath(getcwd()), path.abspath(self.cwd)) self.assertTrue(path.isfile(path.abspath(test_file_path))) except Exception: raise finally: # clean up rmtree(subdir)
def run(self): """ Main method that executes all of the available linters. Returns: An integer corresponding to the number of staged files with linting problems. """ try: # get staged files staged_files_paths = self.git_handle.get_staged_files_paths() # check that paths and file names are ok for _path in staged_files_paths: self.git_handle._check_path_is_allowed(_path) # create a temporary directory tmp_dir = TemporaryDirectory() # write the content of the staged files to temporary files files_in_tmp_dir = [] # list for rel paths of temporary files for rel_path in staged_files_paths: # ensure parent directory of a staged file exists inside of # `tmp_dir` makedirs(path.join( tmp_dir.name, path.dirname(rel_path) ), exist_ok=True) # write content of staged file to a temporary file and # collect relative path in the list tmp_file_path = path.join(tmp_dir.name, rel_path) with open(tmp_file_path, "wb") as tmp_file: content = self.git_handle.get_staged_file_content(rel_path) tmp_file.write(content) files_in_tmp_dir.append( path.relpath(tmp_file_path, tmp_dir.name) ) # get current directory and change directory to temporary directory # (this is to ensure that relative paths are correctly displayed # during linting and that linters run on the staged version of the # files, which are the ones saved in the temporary files); # not changing directory can cause the paths to be interpreted # relatively to the git repository root, which can cause the # linters to run on the version of the files that is currently # in the tree! with exec_in_dir(tmp_dir.name): # initialize a counter to count how many linters return a # non-zero exit status non_zero_linters = 0 for linter in self.linters: # run the linters non_zero_linters += linter.lint(files_in_tmp_dir) return non_zero_linters except Exception: raise finally: tmp_dir.cleanup()