We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Ansible Lint crashes if it is not run from the git repository
##### Summary

Running `ansible-lint --version` from anywhere but a git repository results in a crash with a stacktrace.


##### Issue Type

- Bug Report

##### Ansible and Ansible Lint details

Ansible Lint version: 5.1.1 (cannot paste `--version` output because the program crashes).

- ansible-lint installation method: one of pip, OS package

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->


##### STEPS TO REPRODUCE

Run `ansible-lint --version` from the `/tmp` folder, for example.

<!--- Paste example playbooks or commands between triple backticks below -->
```console (paste below)
$ ansible-lint --version
Traceback (most recent call last):
  File "/home/tadej/tmp/venv/bin/ansible-lint", line 33, in <module>
    sys.exit(load_entry_point('ansible-lint', 'console_scripts', 'ansible-lint')())
  File "/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py", line 299, in _run_cli_entrypoint
    sys.exit(main(sys.argv))
  File "/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py", line 196, in main
    initialize_options(argv[1:])
  File "/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py", line 83, in initialize_options
    new_options = cli.get_config(arguments or [])
  File "/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/cli.py", line 385, in get_config
    project_dir = guess_project_dir(options.config_file)
  File "/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/file_utils.py", line 273, in guess_project_dir
    path = result.stdout.splitlines()[0]
IndexError: list index out of range
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### Desired Behaviour

Ansible Lint should print out its version number.


##### Additional information

`git blame` reported https://github.com/ansible-community/ansible-lint/pull/1661 as the source of the breakage. It was a bit tricky to find the real cause because that one commit contains a bunch of refactoring alongside the actual fix. The error was introduced in https://github.com/ansible-community/ansible-lint/pull/1661/files#diff-4525388e8f05ebb145c2c2366127aba4f3716da965f32a91b70b5c4a0eafd35aL268-L269 because now we do not check the exit code of the git command anymore.


Originally reported by Amin.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/ansiblelint/file_utils.py`
Content:
```
1 """Utility functions related to file operations."""
2 import copy
3 import logging
4 import os
5 import pathlib
6 import subprocess
7 import sys
8 from argparse import Namespace
9 from collections import OrderedDict
10 from contextlib import contextmanager
11 from pathlib import Path
12 from tempfile import NamedTemporaryFile
13 from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Union
14 
15 # import wcmatch
16 import wcmatch.pathlib
17 from wcmatch.wcmatch import RECURSIVE, WcMatch
18 
19 from ansiblelint.config import BASE_KINDS, options
20 from ansiblelint.constants import FileType
21 
22 if TYPE_CHECKING:
23     # https://github.com/PyCQA/pylint/issues/3979
24     BasePathLike = os.PathLike[Any]  # pylint: disable=unsubscriptable-object
25 else:
26     BasePathLike = os.PathLike
27 
28 _logger = logging.getLogger(__package__)
29 
30 
31 def normpath(path: Union[str, BasePathLike]) -> str:
32     """
33     Normalize a path in order to provide a more consistent output.
34 
35     Currently it generates a relative path but in the future we may want to
36     make this user configurable.
37     """
38     # conversion to string in order to allow receiving non string objects
39     relpath = os.path.relpath(str(path))
40     abspath = os.path.abspath(str(path))
41     # we avoid returning relative paths that endup at root level
42     if abspath in relpath:
43         return abspath
44     return relpath
45 
46 
47 @contextmanager
48 def cwd(path: Union[str, BasePathLike]) -> Iterator[None]:
49     """Context manager for temporary changing current working directory."""
50     old_pwd = os.getcwd()
51     os.chdir(path)
52     try:
53         yield
54     finally:
55         os.chdir(old_pwd)
56 
57 
58 def expand_path_vars(path: str) -> str:
59     """Expand the environment or ~ variables in a path string."""
60     # It may be possible for function to be called with a Path object
61     path = str(path).strip()
62     path = os.path.expanduser(path)
63     path = os.path.expandvars(path)
64     return path
65 
66 
67 def expand_paths_vars(paths: List[str]) -> List[str]:
68     """Expand the environment or ~ variables in a list."""
69     paths = [expand_path_vars(p) for p in paths]
70     return paths
71 
72 
73 def kind_from_path(path: Path, base: bool = False) -> FileType:
74     """Determine the file kind based on its name.
75 
76     When called with base=True, it will return the base file type instead
77     of the explicit one. That is expected to return 'yaml' for any yaml files.
78     """
79     # pathlib.Path.match patterns are very limited, they do not support *a*.yml
80     # glob.glob supports **/foo.yml but not multiple extensions
81     pathex = wcmatch.pathlib.PurePath(path.absolute().resolve())
82     kinds = options.kinds if not base else BASE_KINDS
83     for entry in kinds:
84         for k, v in entry.items():
85             if pathex.globmatch(
86                 v,
87                 flags=(
88                     wcmatch.pathlib.GLOBSTAR
89                     | wcmatch.pathlib.BRACE
90                     | wcmatch.pathlib.DOTGLOB
91                 ),
92             ):
93                 return str(k)  # type: ignore
94 
95     if base:
96         # Unknown base file type is default
97         return ""
98 
99     if path.is_dir():
100         return "role"
101 
102     if str(path) == '/dev/stdin':
103         return "playbook"
104 
105     # Unknown file types report a empty string (evaluated as False)
106     return ""
107 
108 
109 class Lintable:
110     """Defines a file/folder that can be linted.
111 
112     Providing file content when creating the object allow creation of in-memory
113     instances that do not need files to be present on disk.
114     """
115 
116     def __init__(
117         self,
118         name: Union[str, Path],
119         content: Optional[str] = None,
120         kind: Optional[FileType] = None,
121     ):
122         """Create a Lintable instance."""
123         # Filename is effective file on disk, for stdin is a namedtempfile
124         self.filename: str = str(name)
125         self.dir: str = ""
126         self.kind: Optional[FileType] = None
127 
128         if isinstance(name, str):
129             self.name = normpath(name)
130             self.path = Path(self.name)
131         else:
132             self.name = str(name)
133             self.path = name
134         self._content = content
135 
136         # if the lintable is part of a role, we save role folder name
137         self.role = ""
138         parts = self.path.parent.parts
139         if 'roles' in parts:
140             role = self.path
141             while role.parent.name != "roles" and role.name:
142                 role = role.parent
143             if role.exists:
144                 self.role = role.name
145 
146         if str(self.path) in ['/dev/stdin', '-']:
147             # pylint: disable=consider-using-with
148             self.file = NamedTemporaryFile(mode="w+", suffix="playbook.yml")
149             self.filename = self.file.name
150             self._content = sys.stdin.read()
151             self.file.write(self._content)
152             self.file.flush()
153             self.path = Path(self.file.name)
154             self.name = 'stdin'
155             self.kind = 'playbook'
156             self.dir = '/'
157         else:
158             self.kind = kind or kind_from_path(self.path)
159         # We store absolute directory in dir
160         if not self.dir:
161             if self.kind == "role":
162                 self.dir = str(self.path.resolve())
163             else:
164                 self.dir = str(self.path.parent.resolve())
165 
166         # determine base file kind (yaml, xml, ini, ...)
167         self.base_kind = kind_from_path(self.path, base=True)
168 
169     def __getitem__(self, key: Any) -> Any:
170         """Provide compatibility subscriptable support."""
171         if key == 'path':
172             return str(self.path)
173         if key == 'type':
174             return str(self.kind)
175         raise NotImplementedError()
176 
177     def get(self, key: Any, default: Any = None) -> Any:
178         """Provide compatibility subscriptable support."""
179         try:
180             return self.__getitem__(key)
181         except NotImplementedError:
182             return default
183 
184     @property
185     def content(self) -> str:
186         """Retried file content, from internal cache or disk."""
187         if self._content is None:
188             with open(self.path, mode='r', encoding='utf-8') as f:
189                 self._content = f.read()
190         return self._content
191 
192     def __hash__(self) -> int:
193         """Return a hash value of the lintables."""
194         return hash((self.name, self.kind))
195 
196     def __eq__(self, other: object) -> bool:
197         """Identify whether the other object represents the same rule match."""
198         if isinstance(other, Lintable):
199             return bool(self.name == other.name and self.kind == other.kind)
200         return False
201 
202     def __repr__(self) -> str:
203         """Return user friendly representation of a lintable."""
204         return f"{self.name} ({self.kind})"
205 
206 
207 def discover_lintables(options: Namespace) -> Dict[str, Any]:
208     """Find all files that we know how to lint."""
209     # git is preferred as it also considers .gitignore
210     git_command_present = [
211         'git',
212         'ls-files',
213         '--cached',
214         '--others',
215         '--exclude-standard',
216         '-z',
217     ]
218     git_command_absent = ['git', 'ls-files', '--deleted', '-z']
219     out = None
220 
221     try:
222         out_present = subprocess.check_output(
223             git_command_present, stderr=subprocess.STDOUT, universal_newlines=True
224         ).split("\x00")[:-1]
225         _logger.info(
226             "Discovered files to lint using: %s", ' '.join(git_command_present)
227         )
228 
229         out_absent = subprocess.check_output(
230             git_command_absent, stderr=subprocess.STDOUT, universal_newlines=True
231         ).split("\x00")[:-1]
232         _logger.info("Excluded removed files using: %s", ' '.join(git_command_absent))
233 
234         out = set(out_present) - set(out_absent)
235     except subprocess.CalledProcessError as exc:
236         if not (exc.returncode == 128 and 'fatal: not a git repository' in exc.output):
237             _logger.warning(
238                 "Failed to discover lintable files using git: %s",
239                 exc.output.rstrip('\n'),
240             )
241     except FileNotFoundError as exc:
242         if options.verbosity:
243             _logger.warning("Failed to locate command: %s", exc)
244 
245     if out is None:
246         exclude_pattern = "|".join(str(x) for x in options.exclude_paths)
247         _logger.info("Looking up for files, excluding %s ...", exclude_pattern)
248         out = WcMatch(
249             '.', exclude_pattern=exclude_pattern, flags=RECURSIVE, limit=256
250         ).match()
251 
252     return OrderedDict.fromkeys(sorted(out))
253 
254 
255 def guess_project_dir(config_file: str) -> str:
256     """Return detected project dir or current working directory."""
257     path = None
258     if config_file is not None:
259         target = pathlib.Path(config_file)
260         if target.exists():
261             path = str(target.parent.absolute())
262 
263     if path is None:
264         try:
265             result = subprocess.run(
266                 ["git", "rev-parse", "--show-toplevel"],
267                 stderr=subprocess.PIPE,
268                 stdout=subprocess.PIPE,
269                 universal_newlines=True,
270                 check=False,
271             )
272 
273             path = result.stdout.splitlines()[0]
274         except subprocess.CalledProcessError as exc:
275             if not (
276                 exc.returncode == 128 and 'fatal: not a git repository' in exc.output
277             ):
278                 _logger.warning(
279                     "Failed to guess project directory using git: %s",
280                     exc.output.rstrip('\n'),
281                 )
282         except FileNotFoundError as exc:
283             _logger.warning("Failed to locate command: %s", exc)
284 
285     if path is None:
286         path = os.getcwd()
287 
288     _logger.info(
289         "Guessed %s as project root directory",
290         path,
291     )
292 
293     return path
294 
295 
296 def expand_dirs_in_lintables(lintables: Set[Lintable]) -> None:
297     """Return all recognized lintables within given directory."""
298     should_expand = False
299 
300     for item in lintables:
301         if item.path.is_dir():
302             should_expand = True
303             break
304 
305     if should_expand:
306         # this relies on git and we do not want to call unless needed
307         all_files = discover_lintables(options)
308 
309         for item in copy.copy(lintables):
310             if item.path.is_dir():
311                 for filename in all_files:
312                     if filename.startswith(str(item.path)):
313                         lintables.add(Lintable(filename))
314
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/ansiblelint/file_utils.py b/src/ansiblelint/file_utils.py
--- a/src/ansiblelint/file_utils.py
+++ b/src/ansiblelint/file_utils.py
@@ -252,7 +252,7 @@
     return OrderedDict.fromkeys(sorted(out))
 
 
-def guess_project_dir(config_file: str) -> str:
+def guess_project_dir(config_file: Optional[str]) -> str:
     """Return detected project dir or current working directory."""
     path = None
     if config_file is not None:
@@ -267,7 +267,7 @@
                 stderr=subprocess.PIPE,
                 stdout=subprocess.PIPE,
                 universal_newlines=True,
-                check=False,
+                check=True,
             )
 
             path = result.stdout.splitlines()[0]

{"golden_diff": "diff --git a/src/ansiblelint/file_utils.py b/src/ansiblelint/file_utils.py\n--- a/src/ansiblelint/file_utils.py\n+++ b/src/ansiblelint/file_utils.py\n@@ -252,7 +252,7 @@\n     return OrderedDict.fromkeys(sorted(out))\n \n \n-def guess_project_dir(config_file: str) -> str:\n+def guess_project_dir(config_file: Optional[str]) -> str:\n     \"\"\"Return detected project dir or current working directory.\"\"\"\n     path = None\n     if config_file is not None:\n@@ -267,7 +267,7 @@\n                 stderr=subprocess.PIPE,\n                 stdout=subprocess.PIPE,\n                 universal_newlines=True,\n-                check=False,\n+                check=True,\n             )\n \n             path = result.stdout.splitlines()[0]\n", "issue": "Ansible Lint crashes if it is not run from the git repository\n##### Summary\r\n\r\nRunning `ansible-lint --version` from anywhere but a git repository results in a crash with a stacktrace.\r\n\r\n\r\n##### Issue Type\r\n\r\n- Bug Report\r\n\r\n##### Ansible and Ansible Lint details\r\n\r\nAnsible Lint version: 5.1.1 (cannot paste `--version` output because the program crashes).\r\n\r\n- ansible-lint installation method: one of pip, OS package\r\n\r\n##### OS / ENVIRONMENT\r\n<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->\r\n\r\n\r\n##### STEPS TO REPRODUCE\r\n\r\nRun `ansible-lint --version` from the `/tmp` folder, for example.\r\n\r\n<!--- Paste example playbooks or commands between triple backticks below -->\r\n```console (paste below)\r\n$ ansible-lint --version\r\nTraceback (most recent call last):\r\n  File \"/home/tadej/tmp/venv/bin/ansible-lint\", line 33, in <module>\r\n    sys.exit(load_entry_point('ansible-lint', 'console_scripts', 'ansible-lint')())\r\n  File \"/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py\", line 299, in _run_cli_entrypoint\r\n    sys.exit(main(sys.argv))\r\n  File \"/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py\", line 196, in main\r\n    initialize_options(argv[1:])\r\n  File \"/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/__main__.py\", line 83, in initialize_options\r\n    new_options = cli.get_config(arguments or [])\r\n  File \"/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/cli.py\", line 385, in get_config\r\n    project_dir = guess_project_dir(options.config_file)\r\n  File \"/home/tadej/xlab/ansible/ansible-lint/src/ansiblelint/file_utils.py\", line 273, in guess_project_dir\r\n    path = result.stdout.splitlines()[0]\r\nIndexError: list index out of range\r\n```\r\n\r\n<!--- HINT: You can paste gist.github.com links for larger files -->\r\n\r\n##### Desired Behaviour\r\n\r\nAnsible Lint should print out its version number.\r\n\r\n\r\n##### Additional information\r\n\r\n`git blame` reported https://github.com/ansible-community/ansible-lint/pull/1661 as the source of the breakage. It was a bit tricky to find the real cause because that one commit contains a bunch of refactoring alongside the actual fix. The error was introduced in https://github.com/ansible-community/ansible-lint/pull/1661/files#diff-4525388e8f05ebb145c2c2366127aba4f3716da965f32a91b70b5c4a0eafd35aL268-L269 because now we do not check the exit code of the git command anymore.\r\n\r\n\r\nOriginally reported by Amin.\n", "before_files": [{"content": "\"\"\"Utility functions related to file operations.\"\"\"\nimport copy\nimport logging\nimport os\nimport pathlib\nimport subprocess\nimport sys\nfrom argparse import Namespace\nfrom collections import OrderedDict\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\nfrom typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Union\n\n# import wcmatch\nimport wcmatch.pathlib\nfrom wcmatch.wcmatch import RECURSIVE, WcMatch\n\nfrom ansiblelint.config import BASE_KINDS, options\nfrom ansiblelint.constants import FileType\n\nif TYPE_CHECKING:\n    # https://github.com/PyCQA/pylint/issues/3979\n    BasePathLike = os.PathLike[Any]  # pylint: disable=unsubscriptable-object\nelse:\n    BasePathLike = os.PathLike\n\n_logger = logging.getLogger(__package__)\n\n\ndef normpath(path: Union[str, BasePathLike]) -> str:\n    \"\"\"\n    Normalize a path in order to provide a more consistent output.\n\n    Currently it generates a relative path but in the future we may want to\n    make this user configurable.\n    \"\"\"\n    # conversion to string in order to allow receiving non string objects\n    relpath = os.path.relpath(str(path))\n    abspath = os.path.abspath(str(path))\n    # we avoid returning relative paths that endup at root level\n    if abspath in relpath:\n        return abspath\n    return relpath\n\n\n@contextmanager\ndef cwd(path: Union[str, BasePathLike]) -> Iterator[None]:\n    \"\"\"Context manager for temporary changing current working directory.\"\"\"\n    old_pwd = os.getcwd()\n    os.chdir(path)\n    try:\n        yield\n    finally:\n        os.chdir(old_pwd)\n\n\ndef expand_path_vars(path: str) -> str:\n    \"\"\"Expand the environment or ~ variables in a path string.\"\"\"\n    # It may be possible for function to be called with a Path object\n    path = str(path).strip()\n    path = os.path.expanduser(path)\n    path = os.path.expandvars(path)\n    return path\n\n\ndef expand_paths_vars(paths: List[str]) -> List[str]:\n    \"\"\"Expand the environment or ~ variables in a list.\"\"\"\n    paths = [expand_path_vars(p) for p in paths]\n    return paths\n\n\ndef kind_from_path(path: Path, base: bool = False) -> FileType:\n    \"\"\"Determine the file kind based on its name.\n\n    When called with base=True, it will return the base file type instead\n    of the explicit one. That is expected to return 'yaml' for any yaml files.\n    \"\"\"\n    # pathlib.Path.match patterns are very limited, they do not support *a*.yml\n    # glob.glob supports **/foo.yml but not multiple extensions\n    pathex = wcmatch.pathlib.PurePath(path.absolute().resolve())\n    kinds = options.kinds if not base else BASE_KINDS\n    for entry in kinds:\n        for k, v in entry.items():\n            if pathex.globmatch(\n                v,\n                flags=(\n                    wcmatch.pathlib.GLOBSTAR\n                    | wcmatch.pathlib.BRACE\n                    | wcmatch.pathlib.DOTGLOB\n                ),\n            ):\n                return str(k)  # type: ignore\n\n    if base:\n        # Unknown base file type is default\n        return \"\"\n\n    if path.is_dir():\n        return \"role\"\n\n    if str(path) == '/dev/stdin':\n        return \"playbook\"\n\n    # Unknown file types report a empty string (evaluated as False)\n    return \"\"\n\n\nclass Lintable:\n    \"\"\"Defines a file/folder that can be linted.\n\n    Providing file content when creating the object allow creation of in-memory\n    instances that do not need files to be present on disk.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: Union[str, Path],\n        content: Optional[str] = None,\n        kind: Optional[FileType] = None,\n    ):\n        \"\"\"Create a Lintable instance.\"\"\"\n        # Filename is effective file on disk, for stdin is a namedtempfile\n        self.filename: str = str(name)\n        self.dir: str = \"\"\n        self.kind: Optional[FileType] = None\n\n        if isinstance(name, str):\n            self.name = normpath(name)\n            self.path = Path(self.name)\n        else:\n            self.name = str(name)\n            self.path = name\n        self._content = content\n\n        # if the lintable is part of a role, we save role folder name\n        self.role = \"\"\n        parts = self.path.parent.parts\n        if 'roles' in parts:\n            role = self.path\n            while role.parent.name != \"roles\" and role.name:\n                role = role.parent\n            if role.exists:\n                self.role = role.name\n\n        if str(self.path) in ['/dev/stdin', '-']:\n            # pylint: disable=consider-using-with\n            self.file = NamedTemporaryFile(mode=\"w+\", suffix=\"playbook.yml\")\n            self.filename = self.file.name\n            self._content = sys.stdin.read()\n            self.file.write(self._content)\n            self.file.flush()\n            self.path = Path(self.file.name)\n            self.name = 'stdin'\n            self.kind = 'playbook'\n            self.dir = '/'\n        else:\n            self.kind = kind or kind_from_path(self.path)\n        # We store absolute directory in dir\n        if not self.dir:\n            if self.kind == \"role\":\n                self.dir = str(self.path.resolve())\n            else:\n                self.dir = str(self.path.parent.resolve())\n\n        # determine base file kind (yaml, xml, ini, ...)\n        self.base_kind = kind_from_path(self.path, base=True)\n\n    def __getitem__(self, key: Any) -> Any:\n        \"\"\"Provide compatibility subscriptable support.\"\"\"\n        if key == 'path':\n            return str(self.path)\n        if key == 'type':\n            return str(self.kind)\n        raise NotImplementedError()\n\n    def get(self, key: Any, default: Any = None) -> Any:\n        \"\"\"Provide compatibility subscriptable support.\"\"\"\n        try:\n            return self.__getitem__(key)\n        except NotImplementedError:\n            return default\n\n    @property\n    def content(self) -> str:\n        \"\"\"Retried file content, from internal cache or disk.\"\"\"\n        if self._content is None:\n            with open(self.path, mode='r', encoding='utf-8') as f:\n                self._content = f.read()\n        return self._content\n\n    def __hash__(self) -> int:\n        \"\"\"Return a hash value of the lintables.\"\"\"\n        return hash((self.name, self.kind))\n\n    def __eq__(self, other: object) -> bool:\n        \"\"\"Identify whether the other object represents the same rule match.\"\"\"\n        if isinstance(other, Lintable):\n            return bool(self.name == other.name and self.kind == other.kind)\n        return False\n\n    def __repr__(self) -> str:\n        \"\"\"Return user friendly representation of a lintable.\"\"\"\n        return f\"{self.name} ({self.kind})\"\n\n\ndef discover_lintables(options: Namespace) -> Dict[str, Any]:\n    \"\"\"Find all files that we know how to lint.\"\"\"\n    # git is preferred as it also considers .gitignore\n    git_command_present = [\n        'git',\n        'ls-files',\n        '--cached',\n        '--others',\n        '--exclude-standard',\n        '-z',\n    ]\n    git_command_absent = ['git', 'ls-files', '--deleted', '-z']\n    out = None\n\n    try:\n        out_present = subprocess.check_output(\n            git_command_present, stderr=subprocess.STDOUT, universal_newlines=True\n        ).split(\"\\x00\")[:-1]\n        _logger.info(\n            \"Discovered files to lint using: %s\", ' '.join(git_command_present)\n        )\n\n        out_absent = subprocess.check_output(\n            git_command_absent, stderr=subprocess.STDOUT, universal_newlines=True\n        ).split(\"\\x00\")[:-1]\n        _logger.info(\"Excluded removed files using: %s\", ' '.join(git_command_absent))\n\n        out = set(out_present) - set(out_absent)\n    except subprocess.CalledProcessError as exc:\n        if not (exc.returncode == 128 and 'fatal: not a git repository' in exc.output):\n            _logger.warning(\n                \"Failed to discover lintable files using git: %s\",\n                exc.output.rstrip('\\n'),\n            )\n    except FileNotFoundError as exc:\n        if options.verbosity:\n            _logger.warning(\"Failed to locate command: %s\", exc)\n\n    if out is None:\n        exclude_pattern = \"|\".join(str(x) for x in options.exclude_paths)\n        _logger.info(\"Looking up for files, excluding %s ...\", exclude_pattern)\n        out = WcMatch(\n            '.', exclude_pattern=exclude_pattern, flags=RECURSIVE, limit=256\n        ).match()\n\n    return OrderedDict.fromkeys(sorted(out))\n\n\ndef guess_project_dir(config_file: str) -> str:\n    \"\"\"Return detected project dir or current working directory.\"\"\"\n    path = None\n    if config_file is not None:\n        target = pathlib.Path(config_file)\n        if target.exists():\n            path = str(target.parent.absolute())\n\n    if path is None:\n        try:\n            result = subprocess.run(\n                [\"git\", \"rev-parse\", \"--show-toplevel\"],\n                stderr=subprocess.PIPE,\n                stdout=subprocess.PIPE,\n                universal_newlines=True,\n                check=False,\n            )\n\n            path = result.stdout.splitlines()[0]\n        except subprocess.CalledProcessError as exc:\n            if not (\n                exc.returncode == 128 and 'fatal: not a git repository' in exc.output\n            ):\n                _logger.warning(\n                    \"Failed to guess project directory using git: %s\",\n                    exc.output.rstrip('\\n'),\n                )\n        except FileNotFoundError as exc:\n            _logger.warning(\"Failed to locate command: %s\", exc)\n\n    if path is None:\n        path = os.getcwd()\n\n    _logger.info(\n        \"Guessed %s as project root directory\",\n        path,\n    )\n\n    return path\n\n\ndef expand_dirs_in_lintables(lintables: Set[Lintable]) -> None:\n    \"\"\"Return all recognized lintables within given directory.\"\"\"\n    should_expand = False\n\n    for item in lintables:\n        if item.path.is_dir():\n            should_expand = True\n            break\n\n    if should_expand:\n        # this relies on git and we do not want to call unless needed\n        all_files = discover_lintables(options)\n\n        for item in copy.copy(lintables):\n            if item.path.is_dir():\n                for filename in all_files:\n                    if filename.startswith(str(item.path)):\n                        lintables.add(Lintable(filename))\n", "path": "src/ansiblelint/file_utils.py"}], "after_files": [{"content": "\"\"\"Utility functions related to file operations.\"\"\"\nimport copy\nimport logging\nimport os\nimport pathlib\nimport subprocess\nimport sys\nfrom argparse import Namespace\nfrom collections import OrderedDict\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\nfrom typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Union\n\n# import wcmatch\nimport wcmatch.pathlib\nfrom wcmatch.wcmatch import RECURSIVE, WcMatch\n\nfrom ansiblelint.config import BASE_KINDS, options\nfrom ansiblelint.constants import FileType\n\nif TYPE_CHECKING:\n    # https://github.com/PyCQA/pylint/issues/3979\n    BasePathLike = os.PathLike[Any]  # pylint: disable=unsubscriptable-object\nelse:\n    BasePathLike = os.PathLike\n\n_logger = logging.getLogger(__package__)\n\n\ndef normpath(path: Union[str, BasePathLike]) -> str:\n    \"\"\"\n    Normalize a path in order to provide a more consistent output.\n\n    Currently it generates a relative path but in the future we may want to\n    make this user configurable.\n    \"\"\"\n    # conversion to string in order to allow receiving non string objects\n    relpath = os.path.relpath(str(path))\n    abspath = os.path.abspath(str(path))\n    # we avoid returning relative paths that endup at root level\n    if abspath in relpath:\n        return abspath\n    return relpath\n\n\n@contextmanager\ndef cwd(path: Union[str, BasePathLike]) -> Iterator[None]:\n    \"\"\"Context manager for temporary changing current working directory.\"\"\"\n    old_pwd = os.getcwd()\n    os.chdir(path)\n    try:\n        yield\n    finally:\n        os.chdir(old_pwd)\n\n\ndef expand_path_vars(path: str) -> str:\n    \"\"\"Expand the environment or ~ variables in a path string.\"\"\"\n    # It may be possible for function to be called with a Path object\n    path = str(path).strip()\n    path = os.path.expanduser(path)\n    path = os.path.expandvars(path)\n    return path\n\n\ndef expand_paths_vars(paths: List[str]) -> List[str]:\n    \"\"\"Expand the environment or ~ variables in a list.\"\"\"\n    paths = [expand_path_vars(p) for p in paths]\n    return paths\n\n\ndef kind_from_path(path: Path, base: bool = False) -> FileType:\n    \"\"\"Determine the file kind based on its name.\n\n    When called with base=True, it will return the base file type instead\n    of the explicit one. That is expected to return 'yaml' for any yaml files.\n    \"\"\"\n    # pathlib.Path.match patterns are very limited, they do not support *a*.yml\n    # glob.glob supports **/foo.yml but not multiple extensions\n    pathex = wcmatch.pathlib.PurePath(path.absolute().resolve())\n    kinds = options.kinds if not base else BASE_KINDS\n    for entry in kinds:\n        for k, v in entry.items():\n            if pathex.globmatch(\n                v,\n                flags=(\n                    wcmatch.pathlib.GLOBSTAR\n                    | wcmatch.pathlib.BRACE\n                    | wcmatch.pathlib.DOTGLOB\n                ),\n            ):\n                return str(k)  # type: ignore\n\n    if base:\n        # Unknown base file type is default\n        return \"\"\n\n    if path.is_dir():\n        return \"role\"\n\n    if str(path) == '/dev/stdin':\n        return \"playbook\"\n\n    # Unknown file types report a empty string (evaluated as False)\n    return \"\"\n\n\nclass Lintable:\n    \"\"\"Defines a file/folder that can be linted.\n\n    Providing file content when creating the object allow creation of in-memory\n    instances that do not need files to be present on disk.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: Union[str, Path],\n        content: Optional[str] = None,\n        kind: Optional[FileType] = None,\n    ):\n        \"\"\"Create a Lintable instance.\"\"\"\n        # Filename is effective file on disk, for stdin is a namedtempfile\n        self.filename: str = str(name)\n        self.dir: str = \"\"\n        self.kind: Optional[FileType] = None\n\n        if isinstance(name, str):\n            self.name = normpath(name)\n            self.path = Path(self.name)\n        else:\n            self.name = str(name)\n            self.path = name\n        self._content = content\n\n        # if the lintable is part of a role, we save role folder name\n        self.role = \"\"\n        parts = self.path.parent.parts\n        if 'roles' in parts:\n            role = self.path\n            while role.parent.name != \"roles\" and role.name:\n                role = role.parent\n            if role.exists:\n                self.role = role.name\n\n        if str(self.path) in ['/dev/stdin', '-']:\n            # pylint: disable=consider-using-with\n            self.file = NamedTemporaryFile(mode=\"w+\", suffix=\"playbook.yml\")\n            self.filename = self.file.name\n            self._content = sys.stdin.read()\n            self.file.write(self._content)\n            self.file.flush()\n            self.path = Path(self.file.name)\n            self.name = 'stdin'\n            self.kind = 'playbook'\n            self.dir = '/'\n        else:\n            self.kind = kind or kind_from_path(self.path)\n        # We store absolute directory in dir\n        if not self.dir:\n            if self.kind == \"role\":\n                self.dir = str(self.path.resolve())\n            else:\n                self.dir = str(self.path.parent.resolve())\n\n        # determine base file kind (yaml, xml, ini, ...)\n        self.base_kind = kind_from_path(self.path, base=True)\n\n    def __getitem__(self, key: Any) -> Any:\n        \"\"\"Provide compatibility subscriptable support.\"\"\"\n        if key == 'path':\n            return str(self.path)\n        if key == 'type':\n            return str(self.kind)\n        raise NotImplementedError()\n\n    def get(self, key: Any, default: Any = None) -> Any:\n        \"\"\"Provide compatibility subscriptable support.\"\"\"\n        try:\n            return self.__getitem__(key)\n        except NotImplementedError:\n            return default\n\n    @property\n    def content(self) -> str:\n        \"\"\"Retried file content, from internal cache or disk.\"\"\"\n        if self._content is None:\n            with open(self.path, mode='r', encoding='utf-8') as f:\n                self._content = f.read()\n        return self._content\n\n    def __hash__(self) -> int:\n        \"\"\"Return a hash value of the lintables.\"\"\"\n        return hash((self.name, self.kind))\n\n    def __eq__(self, other: object) -> bool:\n        \"\"\"Identify whether the other object represents the same rule match.\"\"\"\n        if isinstance(other, Lintable):\n            return bool(self.name == other.name and self.kind == other.kind)\n        return False\n\n    def __repr__(self) -> str:\n        \"\"\"Return user friendly representation of a lintable.\"\"\"\n        return f\"{self.name} ({self.kind})\"\n\n\ndef discover_lintables(options: Namespace) -> Dict[str, Any]:\n    \"\"\"Find all files that we know how to lint.\"\"\"\n    # git is preferred as it also considers .gitignore\n    git_command_present = [\n        'git',\n        'ls-files',\n        '--cached',\n        '--others',\n        '--exclude-standard',\n        '-z',\n    ]\n    git_command_absent = ['git', 'ls-files', '--deleted', '-z']\n    out = None\n\n    try:\n        out_present = subprocess.check_output(\n            git_command_present, stderr=subprocess.STDOUT, universal_newlines=True\n        ).split(\"\\x00\")[:-1]\n        _logger.info(\n            \"Discovered files to lint using: %s\", ' '.join(git_command_present)\n        )\n\n        out_absent = subprocess.check_output(\n            git_command_absent, stderr=subprocess.STDOUT, universal_newlines=True\n        ).split(\"\\x00\")[:-1]\n        _logger.info(\"Excluded removed files using: %s\", ' '.join(git_command_absent))\n\n        out = set(out_present) - set(out_absent)\n    except subprocess.CalledProcessError as exc:\n        if not (exc.returncode == 128 and 'fatal: not a git repository' in exc.output):\n            _logger.warning(\n                \"Failed to discover lintable files using git: %s\",\n                exc.output.rstrip('\\n'),\n            )\n    except FileNotFoundError as exc:\n        if options.verbosity:\n            _logger.warning(\"Failed to locate command: %s\", exc)\n\n    if out is None:\n        exclude_pattern = \"|\".join(str(x) for x in options.exclude_paths)\n        _logger.info(\"Looking up for files, excluding %s ...\", exclude_pattern)\n        out = WcMatch(\n            '.', exclude_pattern=exclude_pattern, flags=RECURSIVE, limit=256\n        ).match()\n\n    return OrderedDict.fromkeys(sorted(out))\n\n\ndef guess_project_dir(config_file: Optional[str]) -> str:\n    \"\"\"Return detected project dir or current working directory.\"\"\"\n    path = None\n    if config_file is not None:\n        target = pathlib.Path(config_file)\n        if target.exists():\n            path = str(target.parent.absolute())\n\n    if path is None:\n        try:\n            result = subprocess.run(\n                [\"git\", \"rev-parse\", \"--show-toplevel\"],\n                stderr=subprocess.PIPE,\n                stdout=subprocess.PIPE,\n                universal_newlines=True,\n                check=True,\n            )\n\n            path = result.stdout.splitlines()[0]\n        except subprocess.CalledProcessError as exc:\n            if not (\n                exc.returncode == 128 and 'fatal: not a git repository' in exc.output\n            ):\n                _logger.warning(\n                    \"Failed to guess project directory using git: %s\",\n                    exc.output.rstrip('\\n'),\n                )\n        except FileNotFoundError as exc:\n            _logger.warning(\"Failed to locate command: %s\", exc)\n\n    if path is None:\n        path = os.getcwd()\n\n    _logger.info(\n        \"Guessed %s as project root directory\",\n        path,\n    )\n\n    return path\n\n\ndef expand_dirs_in_lintables(lintables: Set[Lintable]) -> None:\n    \"\"\"Return all recognized lintables within given directory.\"\"\"\n    should_expand = False\n\n    for item in lintables:\n        if item.path.is_dir():\n            should_expand = True\n            break\n\n    if should_expand:\n        # this relies on git and we do not want to call unless needed\n        all_files = discover_lintables(options)\n\n        for item in copy.copy(lintables):\n            if item.path.is_dir():\n                for filename in all_files:\n                    if filename.startswith(str(item.path)):\n                        lintables.add(Lintable(filename))\n", "path": "src/ansiblelint/file_utils.py"}]}