1#!/usr/bin/env python
2"""Tests various operations using the cola.git module
3"""
4from __future__ import absolute_import, division, unicode_literals
5import unittest
6
7try:
8    from unittest.mock import patch
9except ImportError:
10    from mock import patch
11
12from cola import git
13from cola.git import STDOUT
14
15
16class GitModuleTestCase(unittest.TestCase):
17    @patch('cola.git.is_git_dir')
18    def test_find_git_dir_None(self, is_git_dir):
19
20        paths = git.find_git_directory(None)
21
22        self.assertFalse(is_git_dir.called)
23        self.assertEqual(None, paths.git_dir)
24        self.assertEqual(None, paths.git_file)
25        self.assertEqual(None, paths.worktree)
26
27    @patch('cola.git.is_git_dir')
28    def test_find_git_dir_empty_string(self, is_git_dir):
29
30        paths = git.find_git_directory('')
31
32        self.assertFalse(is_git_dir.called)
33        self.assertEqual(None, paths.git_dir)
34        self.assertEqual(None, paths.git_file)
35        self.assertEqual(None, paths.worktree)
36
37    @patch('cola.git.is_git_dir')
38    def test_find_git_dir_never_found(self, is_git_dir):
39        is_git_dir.return_value = False
40
41        paths = git.find_git_directory('/does/not/exist')
42
43        self.assertTrue(is_git_dir.called)
44        self.assertEqual(None, paths.git_dir)
45        self.assertEqual(None, paths.git_file)
46        self.assertEqual(None, paths.worktree)
47
48        self.assertEqual(8, is_git_dir.call_count)
49        kwargs = {}
50        is_git_dir.assert_has_calls(
51            [
52                (('/does/not/exist',), kwargs),
53                (('/does/not/exist/.git',), kwargs),
54                (('/does/not',), kwargs),
55                (('/does/not/.git',), kwargs),
56                (('/does',), kwargs),
57                (('/does/.git',), kwargs),
58                (('/',), kwargs),
59                (('/.git',), kwargs),
60            ]
61        )
62
63    @patch('cola.git.is_git_dir')
64    def test_find_git_dir_found_right_away(self, is_git_dir):
65        git_dir = '/seems/to/exist/.git'
66        worktree = '/seems/to/exist'
67        is_git_dir.return_value = True
68
69        paths = git.find_git_directory(git_dir)
70
71        self.assertTrue(is_git_dir.called)
72        self.assertEqual(git_dir, paths.git_dir)
73        self.assertEqual(None, paths.git_file)
74        self.assertEqual(worktree, paths.worktree)
75
76    @patch('cola.git.is_git_dir')
77    def test_find_git_does_discovery(self, is_git_dir):
78        git_dir = '/the/root/.git'
79        worktree = '/the/root'
80        is_git_dir.side_effect = lambda x: x == git_dir
81
82        paths = git.find_git_directory('/the/root/sub/dir')
83
84        self.assertEqual(git_dir, paths.git_dir)
85        self.assertEqual(None, paths.git_file)
86        self.assertEqual(worktree, paths.worktree)
87
88    @patch('cola.git.read_git_file')
89    @patch('cola.git.is_git_file')
90    @patch('cola.git.is_git_dir')
91    def test_find_git_honors_git_files(self, is_git_dir, is_git_file, read_git_file):
92        git_file = '/the/root/.git'
93        worktree = '/the/root'
94        git_dir = '/super/module/.git/modules/root'
95
96        is_git_dir.side_effect = lambda x: x == git_file
97        is_git_file.side_effect = lambda x: x == git_file
98        read_git_file.return_value = git_dir
99
100        paths = git.find_git_directory('/the/root/sub/dir')
101
102        self.assertEqual(git_dir, paths.git_dir)
103        self.assertEqual(git_file, paths.git_file)
104        self.assertEqual(worktree, paths.worktree)
105
106        kwargs = {}
107        self.assertEqual(6, is_git_dir.call_count)
108        is_git_dir.assert_has_calls(
109            [
110                (('/the/root/sub/dir',), kwargs),
111                (('/the/root/sub/dir/.git',), kwargs),
112                (('/the/root/sub',), kwargs),
113                (('/the/root/sub/.git',), kwargs),
114                (('/the/root',), kwargs),
115                (('/the/root/.git',), kwargs),
116            ]
117        )
118        read_git_file.assert_called_once_with('/the/root/.git')
119
120    @patch('cola.core.getenv')
121    @patch('cola.git.is_git_dir')
122    def test_find_git_honors_ceiling_dirs(self, is_git_dir, getenv):
123
124        git_dir = '/ceiling/.git'
125        ceiling = '/tmp:/ceiling:/other/ceiling'
126        is_git_dir.side_effect = lambda x: x == git_dir
127
128        def mock_getenv(k, v=None):
129            if k == 'GIT_CEILING_DIRECTORIES':
130                return ceiling
131            return v
132
133        getenv.side_effect = mock_getenv
134
135        paths = git.find_git_directory('/ceiling/sub/dir')
136
137        self.assertEqual(None, paths.git_dir)
138        self.assertEqual(None, paths.git_file)
139        self.assertEqual(None, paths.worktree)
140
141        self.assertEqual(4, is_git_dir.call_count)
142        kwargs = {}
143        is_git_dir.assert_has_calls(
144            [
145                (('/ceiling/sub/dir',), kwargs),
146                (('/ceiling/sub/dir/.git',), kwargs),
147                (('/ceiling/sub',), kwargs),
148                (('/ceiling/sub/.git',), kwargs),
149            ]
150        )
151
152    @patch('cola.core.islink')
153    @patch('cola.core.isdir')
154    @patch('cola.core.isfile')
155    def test_is_git_dir_finds_linked_repository(self, isfile, isdir, islink):
156        dirs = set(
157            [
158                '/foo',
159                '/foo/.git',
160                '/foo/.git/refs',
161                '/foo/.git/objects',
162                '/foo/.git/worktrees',
163                '/foo/.git/worktrees/foo',
164            ]
165        )
166        files = set(
167            [
168                '/foo/.git/HEAD',
169                '/foo/.git/worktrees/foo/HEAD',
170                '/foo/.git/worktrees/foo/index',
171                '/foo/.git/worktrees/foo/commondir',
172                '/foo/.git/worktrees/foo/gitdir',
173            ]
174        )
175        islink.return_value = False
176        isfile.side_effect = lambda x: x in files
177        isdir.side_effect = lambda x: x in dirs
178
179        self.assertTrue(git.is_git_dir('/foo/.git/worktrees/foo'))
180        self.assertTrue(git.is_git_dir('/foo/.git'))
181
182    @patch('cola.core.getenv')
183    @patch('cola.git.is_git_dir')
184    def test_find_git_worktree_from_GIT_DIR(self, is_git_dir, getenv):
185        git_dir = '/repo/.git'
186        worktree = '/repo'
187        is_git_dir.return_value = True
188        getenv.side_effect = lambda x: x == 'GIT_DIR' and '/repo/.git' or None
189
190        paths = git.find_git_directory(git_dir)
191        self.assertTrue(is_git_dir.called)
192        self.assertEqual(git_dir, paths.git_dir)
193        self.assertEqual(None, paths.git_file)
194        self.assertEqual(worktree, paths.worktree)
195
196    @patch('cola.git.is_git_dir')
197    def test_finds_no_worktree_from_bare_repo(self, is_git_dir):
198        git_dir = '/repos/bare.git'
199        worktree = None
200        is_git_dir.return_value = True
201
202        paths = git.find_git_directory(git_dir)
203        self.assertTrue(is_git_dir.called)
204        self.assertEqual(git_dir, paths.git_dir)
205        self.assertEqual(None, paths.git_file)
206        self.assertEqual(worktree, paths.worktree)
207
208    @patch('cola.core.getenv')
209    @patch('cola.git.is_git_dir')
210    def test_find_git_directory_uses_GIT_WORK_TREE(self, is_git_dir, getenv):
211        git_dir = '/repo/worktree/.git'
212        worktree = '/repo/worktree'
213
214        def is_git_dir_fn(path):
215            return path == git_dir
216
217        is_git_dir.side_effect = is_git_dir_fn
218
219        def getenv_fn(name):
220            if name == 'GIT_WORK_TREE':
221                return worktree
222            return None
223
224        getenv.side_effect = getenv_fn
225
226        paths = git.find_git_directory(worktree)
227        self.assertTrue(is_git_dir.called)
228        self.assertEqual(git_dir, paths.git_dir)
229        self.assertEqual(None, paths.git_file)
230        self.assertEqual(worktree, paths.worktree)
231
232    @patch('cola.core.getenv')
233    @patch('cola.git.is_git_dir')
234    def test_uses_cwd_for_worktree_with_GIT_DIR(self, is_git_dir, getenv):
235        git_dir = '/repo/.yadm/repo.git'
236        worktree = '/repo'
237
238        def getenv_fn(name):
239            if name == 'GIT_DIR':
240                return git_dir
241            return None
242
243        getenv.side_effect = getenv_fn
244
245        def is_git_dir_fn(path):
246            return path == git_dir
247
248        is_git_dir.side_effect = is_git_dir_fn
249
250        paths = git.find_git_directory(worktree)
251        self.assertTrue(is_git_dir.called)
252        self.assertTrue(getenv.called)
253        self.assertEqual(git_dir, paths.git_dir)
254        self.assertEqual(None, paths.git_file)
255        self.assertEqual(worktree, paths.worktree)
256
257
258class GitCommandTest(unittest.TestCase):
259    """Runs tests using a git.Git instance"""
260
261    def setUp(self):
262        """Creates a git.Git instance for later use"""
263        self.git = git.Git()
264
265    def test_transform_kwargs_empty(self):
266        expect = []
267        actual = git.transform_kwargs(foo=None, bar=False)
268        self.assertEqual(expect, actual)
269
270    def test_transform_kwargs_single_dash_from_True(self):
271        """Single dash for one-character True"""
272        expect = ['-a']
273        actual = git.transform_kwargs(a=True)
274        self.assertEqual(expect, actual)
275
276    def test_transform_kwargs_no_single_dash_from_False(self):
277        """No single-dash for False"""
278        expect = []
279        actual = git.transform_kwargs(a=False)
280        self.assertEqual(expect, actual)
281
282    def test_transform_kwargs_double_dash_from_True(self):
283        """Double-dash for longer True"""
284        expect = ['--abc']
285        actual = git.transform_kwargs(abc=True)
286        self.assertEqual(expect, actual)
287
288    def test_transform_kwargs_no_double_dash_from_True(self):
289        """No double-dash for False"""
290        expect = []
291        actual = git.transform_kwargs(abc=False)
292        self.assertEqual(expect, actual)
293
294    def test_transform_kwargs_single_dash_int(self):
295        expect = ['-a1']
296        actual = git.transform_kwargs(a=1)
297        self.assertEqual(expect, actual)
298
299    def test_transform_kwargs_double_dash_int(self):
300        expect = ['--abc=1']
301        actual = git.transform_kwargs(abc=1)
302        self.assertEqual(expect, actual)
303
304    def test_transform_kwargs_single_dash_float(self):
305        expect = ['-a1.5']
306        actual = git.transform_kwargs(a=1.5)
307        self.assertEqual(expect, actual)
308
309    def test_transform_kwargs_double_dash_float(self):
310        expect = ['--abc=1.5']
311        actual = git.transform_kwargs(abc=1.5)
312        self.assertEqual(expect, actual)
313
314    def test_transform_kwargs_single_dash_string(self):
315        expect = ['-abc']
316        actual = git.transform_kwargs(a='bc')
317        self.assertEqual(expect, actual)
318
319    def test_transform_double_single_dash_string(self):
320        expect = ['--abc=def']
321        actual = git.transform_kwargs(abc='def')
322        self.assertEqual(expect, actual)
323
324    def test_version(self):
325        """Test running 'git version'"""
326        version = self.git.version()[STDOUT]
327        self.assertTrue(version.startswith('git version'))
328
329    def test_stdout(self):
330        """Test overflowing the stdout buffer"""
331        # Write to stdout only
332        code = r'import sys; s = "\0" * (1024 * 16 + 1); sys.stdout.write(s);'
333        status, out, err = git.Git.execute(['python', '-c', code], _raw=True)
334        self.assertEqual(status, 0)
335        self.assertEqual(len(out), 1024 * 16 + 1)
336        self.assertEqual(len(err), 0)
337
338    def test_stderr(self):
339        """Test that stderr is seen"""
340        # Write to stderr and capture it
341        code = r'import sys; s = "\0" * (1024 * 16 + 1); sys.stderr.write(s);'
342        status, out, err = git.Git.execute(['python', '-c', code], _raw=True)
343        self.assertEqual(status, 0)
344        self.assertEqual(len(out), 0)
345        self.assertEqual(len(err), 1024 * 16 + 1)
346
347    def test_stdout_and_stderr(self):
348        """Test ignoring stderr when stdout+stderr are provided (v2)"""
349        # Write to stdout and stderr but only capture stdout
350        code = (
351            'import sys;'
352            's = "\\0" * (1024 * 16 + 1);'
353            'sys.stdout.write(s);'
354            'sys.stderr.write(s);'
355        )
356        status, out, err = git.Git.execute(['python', '-c', code], _raw=True)
357        self.assertEqual(status, 0)
358        self.assertEqual(len(out), 1024 * 16 + 1)
359        self.assertEqual(len(err), 1024 * 16 + 1)
360
361    def test_it_doesnt_deadlock(self):
362        """Test that we don't deadlock with both stderr and stdout"""
363        # 16k+1 bytes to exhaust any output buffers
364        code = (
365            'import sys;'
366            's = "\\0" * (1024 * 16 + 1);'
367            'sys.stderr.write(s);'
368            'sys.stdout.write(s);'
369        )
370        status, out, err = git.Git.execute(['python', '-c', code], _raw=True)
371        self.assertEqual(status, 0)
372        self.assertEqual(out, '\0' * (1024 * 16 + 1))
373        self.assertEqual(err, '\0' * (1024 * 16 + 1))
374
375
376if __name__ == '__main__':
377    unittest.main()
378