55# the BSD License: http://www.opensource.org/licenses/bsd-license.php
66from __future__ import print_function
77
8+ import contextlib
89from functools import wraps
910import io
1011import logging
1112import os
12- import tempfile
13+ import tempfile # @UnusedImport
1314import textwrap
1415import time
1516from unittest import TestCase
1617
1718from git .compat import string_types , is_win
18- from git .util import rmtree
19-
19+ from git .util import rmtree , cwd
2020import os .path as osp
2121
2222
23+ try :
24+ from unittest import mock
25+ from contextlib import ExitStack
26+ from tempfile import TemporaryDirectory
27+ except ImportError : # PY2
28+ import mock
29+ from contextlib2 import ExitStack # @UnusedImport
30+ from backports .tempfile import TemporaryDirectory # @UnusedImport
31+
32+
2333ospd = osp .dirname
2434
2535GIT_REPO = os .environ .get ("GIT_PYTHON_TEST_GIT_REPO_BASE" , ospd (ospd (ospd (ospd (__file__ )))))
2636GIT_DAEMON_PORT = os .environ .get ("GIT_PYTHON_TEST_GIT_DAEMON_PORT" , "19418" )
2737
2838__all__ = (
2939 'fixture_path' , 'fixture' , 'StringProcessAdapter' ,
30- 'with_rw_directory' , 'with_rw_repo' , 'with_rw_and_rw_remote_repo ' , 'TestBase' , 'TestCase' ,
40+ 'with_rw_directory' , 'with_rw_repo' , 'rw_and_rw_remote_repos ' , 'TestBase' , 'TestCase' ,
3141 'GIT_REPO' , 'GIT_DAEMON_PORT'
3242)
3343
@@ -162,39 +172,91 @@ def repo_creator(self):
162172 return argument_passer
163173
164174
175+ @contextlib .contextmanager
165176def launch_git_daemon (base_path , ip , port ):
166- from git import Git
167- if is_win :
168- ## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
169- # but if invoked as 'git daemon', it detaches from parent `git` cmd,
170- # and then CANNOT DIE!
171- # So, invoke it as a single command.
172- ## Cygwin-git has no daemon. But it can use MINGW's.
173- #
174- daemon_cmd = ['git-daemon' ,
175- '--enable=receive-pack' ,
176- '--listen=%s' % ip ,
177- '--port=%s' % port ,
178- '--base-path=%s' % base_path ,
179- base_path ]
180- gd = Git ().execute (daemon_cmd , as_process = True )
181- else :
182- gd = Git ().daemon (base_path ,
183- enable = 'receive-pack' ,
184- listen = ip ,
185- port = port ,
186- base_path = base_path ,
187- as_process = True )
188- # yes, I know ... fortunately, this is always going to work if sleep time is just large enough
189- time .sleep (0.5 )
190- return gd
191-
192-
193- def with_rw_and_rw_remote_repo (working_tree_ref ):
177+ from git import Git # Avoid circular deps.
178+
179+ gd_launched = False
180+ try :
181+ if is_win :
182+ ## On MINGW-git, daemon exists in .\Git\mingw64\libexec\git-core\,
183+ # but if invoked as 'git daemon', it detaches from parent `git` cmd,
184+ # and then CANNOT DIE!
185+ # So, invoke it as a single command.
186+ ## Cygwin-git has no daemon. But it can use MINGW's.
187+ #
188+ daemon_cmd = ['git-daemon' ,
189+ '--enable=receive-pack' ,
190+ '--listen=%s' % ip ,
191+ '--port=%s' % port ,
192+ '--base-path=%s' % base_path ,
193+ base_path ]
194+ gd = Git ().execute (daemon_cmd , as_process = True )
195+ else :
196+ gd = Git ().daemon (base_path ,
197+ enable = 'receive-pack' ,
198+ listen = ip ,
199+ port = port ,
200+ base_path = base_path ,
201+ as_process = True )
202+ gd_launched = True
203+ # yes, I know ... fortunately, this is always going to work if sleep time is just large enough
204+ time .sleep (0.5 * (1 + is_win ))
205+
206+ yield gd
207+
208+ except Exception as ex :
209+ msg = textwrap .dedent ("""
210+ Launching git-daemon failed due to: %s
211+ Probably test will fail subsequently.
212+
213+ BUT you may start *git-daemon* manually with this command:"
214+ git daemon --enable=receive-pack --listen=%s --port=%s --base-path=%s %s
215+ You may also run the daemon on a different port by passing --port=<port>"
216+ and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
217+ """ )
218+ if is_win :
219+ msg += textwrap .dedent ("""
220+
221+ On Windows,
222+ the `git-daemon.exe` must be in PATH.
223+ For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear.
224+ CYGWIN has no daemon, but if one exists, it gets along fine (but has also paths problems).""" )
225+ log .warning (msg , ex , ip , port , base_path , base_path , exc_info = 1 )
226+
227+ yield mock .MagicMock () # @UndefinedVariable
228+
229+ finally :
230+ if gd_launched :
231+ try :
232+ log .debug ("Killing git-daemon..." )
233+ gd .proc .kill ()
234+ except Exception as ex :
235+ ## Either it has died (and we're here), or it won't die, again here...
236+ log .debug ("Hidden error while Killing git-daemon: %s" , ex , exc_info = 1 )
237+
238+
239+ @contextlib .contextmanager
240+ def tmp_clone (repo , clone_prefix , ** clone_kwargs ):
241+ def cleanup_clone (repo ):
242+ repo .git .clear_cache ()
243+ import gc
244+ gc .collect ()
245+
246+ with ExitStack () as stack :
247+ clone_dir = stack .enter_context (TemporaryDirectory (prefix = clone_prefix ))
248+ clone = repo .clone (clone_dir , ** clone_kwargs )
249+ stack .callback (cleanup_clone , clone )
250+
251+ yield clone
252+
253+
254+ @contextlib .contextmanager
255+ def rw_and_rw_remote_repos (repo , working_tree_ref ):
194256 """
195- Same as with_rw_repo, but also provides a writable remote repository from which the
196- rw_repo has been forked as well as a handle for a git-daemon that may be started to
197- run the remote_repo.
257+ A context-manager creating the same temporary-repo as `with_rw_repo` and in addition
258+ a writable remote non-bare repository from which the rw_repo has been forked as well as a handle
259+ for a git-daemon that may be started to run the remote_repo.
198260 The remote repository was cloned as bare repository from the rorepo, wheras
199261 the rw repo has a working tree and was cloned from the remote repository.
200262
@@ -203,11 +265,13 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
203265 and should be an inetd service that serves tempdir.gettempdir() and all
204266 directories in it.
205267
206- The following scetch demonstrates this::
207- rorepo ---<bare clone>---> rw_remote_repo ---<clone>---> rw_repo
268+ The following sketch demonstrates this::
269+ rorepo ---<bare clone>---> remote_repo ---<clone>---> rw_repo
270+
271+ It is used like that::
208272
209- The test case needs to support the following signature: :
210- def case(self, rw_repo, rw_remote_repo)
273+ with rw_and_rw_remote_repos(origin_repo) as (rw_repo, remote_repo) :
274+ ...
211275
212276 This setup allows you to test push and pull scenarios and hooks nicely.
213277
@@ -218,105 +282,51 @@ def case(self, rw_repo, rw_remote_repo)
218282
219283 assert isinstance (working_tree_ref , string_types ), "Decorator requires ref name for working tree checkout"
220284
221- def argument_passer (func ):
222-
223- @wraps (func )
224- def remote_repo_creator (self ):
225- remote_repo_dir = _mktemp ("remote_repo_%s" % func .__name__ )
226- repo_dir = _mktemp ("remote_clone_non_bare_repo" )
227-
228- rw_remote_repo = self .rorepo .clone (remote_repo_dir , shared = True , bare = True )
229- # recursive alternates info ?
230- rw_repo = rw_remote_repo .clone (repo_dir , shared = True , bare = False , n = True )
231- rw_repo .head .commit = working_tree_ref
232- rw_repo .head .reference .checkout ()
233-
234- # prepare for git-daemon
235- rw_remote_repo .daemon_export = True
236-
237- # this thing is just annoying !
238- with rw_remote_repo .config_writer () as crw :
239- section = "daemon"
240- try :
241- crw .add_section (section )
242- except Exception :
243- pass
244- crw .set (section , "receivepack" , True )
245-
246- # Initialize the remote - first do it as local remote and pull, then
247- # we change the url to point to the daemon.
248- d_remote = Remote .create (rw_repo , "daemon_origin" , remote_repo_dir )
249- d_remote .fetch ()
250-
251- base_path , rel_repo_dir = osp .split (remote_repo_dir )
252-
253- remote_repo_url = Git .polish_url ("git://localhost:%s/%s" % (GIT_DAEMON_PORT , rel_repo_dir ))
254- with d_remote .config_writer as cw :
255- cw .set ('url' , remote_repo_url )
256-
285+ with ExitStack () as stack :
286+ rw_remote_repo = stack .enter_context (tmp_clone (repo ,
287+ clone_prefix = "remote_bare_repo_%s" ,
288+ shared = True ,
289+ bare = True ))
290+ rw_repo = stack .enter_context (tmp_clone (rw_remote_repo ,
291+ clone_prefix = "remote_clone_non_bare_repo_" ,
292+ shared = True ,
293+ bare = False ,
294+ n = True ))
295+ remote_repo_dir = rw_remote_repo .working_dir
296+
297+ # recursive alternates info ?
298+ rw_repo .head .commit = working_tree_ref
299+ rw_repo .head .reference .checkout ()
300+
301+ # Allow git-daemon in bare-repo (https://git-scm.com/book/en/v2/Git-on-the-Server-Git-Daemon).
302+ rw_remote_repo .daemon_export = True
303+
304+ section = "daemon"
305+ with rw_remote_repo .config_writer () as crw :
257306 try :
258- gd = launch_git_daemon (Git .polish_url (base_path ), '127.0.0.1' , GIT_DAEMON_PORT )
259- except Exception as ex :
260- if is_win :
261- msg = textwrap .dedent ("""
262- The `git-daemon.exe` must be in PATH.
263- For MINGW, look into .\Git\mingw64\libexec\git-core\), but problems with paths might appear.
264- CYGWIN has no daemon, but if one exists, it gets along fine (has also paths problems)
265- Anyhow, alternatively try starting `git-daemon` manually:""" )
266- else :
267- msg = "Please try starting `git-daemon` manually:"
268- msg += textwrap .dedent ("""
269- git daemon --enable=receive-pack --base-path=%s %s
270- You can also run the daemon on a different port by passing --port=<port>"
271- and setting the environment variable GIT_PYTHON_TEST_GIT_DAEMON_PORT to <port>
272- """ % (base_path , base_path ))
273- raise AssertionError (ex , msg )
274- # END make assertion
275- else :
276- # Try listing remotes, to diagnose whether the daemon is up.
277- rw_repo .git .ls_remote (d_remote )
278-
279- # adjust working dir
280- prev_cwd = os .getcwd ()
281- os .chdir (rw_repo .working_dir )
282-
283- try :
284- return func (self , rw_repo , rw_remote_repo )
285- except :
286- log .info ("Keeping repos after failure: repo_dir = %s, remote_repo_dir = %s" ,
287- repo_dir , remote_repo_dir )
288- repo_dir = remote_repo_dir = None
289- raise
290- finally :
291- os .chdir (prev_cwd )
307+ crw .add_section (section ) # TODO: Add section if not exists.
308+ except Exception :
309+ pass
310+ crw .set (section , "receivepack" , True )
292311
293- finally :
294- try :
295- log .debug ("Killing git-daemon..." )
296- gd .proc .kill ()
297- except :
298- ## Either it has died (and we're here), or it won't die, again here...
299- pass
312+ # Initialize the non-bare repo - first do it as local remote and pull, then
313+ # we change the URL to point to the "relative" against "daemon's `--base-path`.
314+ #
315+ d_remote = Remote .create (rw_repo , "daemon_origin" , remote_repo_dir )
316+ d_remote .fetch ()
317+ base_path , rel_repo_dir = osp .split (remote_repo_dir )
318+ remote_repo_url = Git .polish_url ("git://localhost:%s/%s" % (GIT_DAEMON_PORT , rel_repo_dir ))
319+ with d_remote .config_writer as cw :
320+ cw .set ('url' , remote_repo_url )
300321
301- rw_repo .git .clear_cache ()
302- rw_remote_repo .git .clear_cache ()
303- rw_repo = rw_remote_repo = None
304- import gc
305- gc .collect ()
306- if repo_dir :
307- rmtree (repo_dir )
308- if remote_repo_dir :
309- rmtree (remote_repo_dir )
322+ stack .enter_context (launch_git_daemon (Git .polish_url (base_path ), '127.0.0.1' , GIT_DAEMON_PORT ))
310323
311- if gd is not None :
312- gd .proc .wait ()
313- # END cleanup
314- # END bare repo creator
315- return remote_repo_creator
316- # END remote repo creator
317- # END argument parser
324+ # Try listing remotes, to diagnose whether the daemon is up.
325+ rw_repo .git .ls_remote (d_remote )
318326
319- return argument_passer
327+ # adjust working dir
328+ stack .enter_context (cwd (rw_repo .working_dir ))
329+ yield rw_repo , rw_remote_repo
320330
321331#} END decorators
322332
0 commit comments