55import logging
66import os
77import re
8- import shutil
98import sys
9+ import tarfile
1010from datetime import datetime
1111from subprocess import CalledProcessError , PIPE , Popen , STDOUT
1212
13- from sphinxcontrib .versioning .lib import TempDir
14-
1513IS_WINDOWS = sys .platform == 'win32'
1614RE_ALL_REMOTES = re .compile (r'([\w./-]+)\t([A-Za-z0-9@:/\\._-]+) \((fetch|push)\)\n' )
1715RE_REMOTE = re .compile (r'^(?P<sha>[0-9a-f]{5,40})\trefs/(?P<kind>heads|tags)/(?P<name>[\w./-]+(?:\^\{})?)$' ,
@@ -113,15 +111,15 @@ def chunk(iterator, max_size):
113111 yield chunked
114112
115113
116- def run_command (local_root , command , env_var = True , piped = None ):
117- """Run a command and return the output. Run another command and pipe its output to the primary command.
114+ def run_command (local_root , command , env_var = True , pipeto = None ):
115+ """Run a command and return the output.
118116
119117 :raise CalledProcessError: Command exits non-zero.
120118
121119 :param str local_root: Local path to git root directory.
122120 :param iter command: Command to run.
123121 :param bool env_var: Define GIT_DIR environment variable (on non-Windows).
124- :param iter piped: Second command to pipe its stdout to `command`'s stdin .
122+ :param function pipeto: Pipe ` command`'s stdout to this function (only parameter given) .
125123
126124 :return: Command output.
127125 :rtype: str
@@ -135,26 +133,17 @@ def run_command(local_root, command, env_var=True, piped=None):
135133 else :
136134 env .pop ('GIT_DIR' , None )
137135
138- # Start commands .
136+ # Run command .
139137 with open (os .devnull ) as null :
140- parent = Popen (piped , cwd = local_root , env = env , stdout = PIPE , stderr = PIPE , stdin = null ) if piped else None
141- stdin = parent .stdout if piped else null
142- main = Popen (command , cwd = local_root , env = env , stdout = PIPE , stderr = STDOUT , stdin = stdin )
143-
144- # Wait for commands and log.
145- common_dict = dict (cwd = local_root , stdin = None )
146- if piped :
147- main .wait () # Let main command read parent.stdout before parent.communicate() does.
148- parent_output = parent .communicate ()[1 ].decode ('utf-8' )
149- log .debug (json .dumps (dict (common_dict , command = piped , code = parent .poll (), output = parent_output )))
150- else :
151- parent_output = ''
152- main_output = main .communicate ()[0 ].decode ('utf-8' )
153- log .debug (json .dumps (dict (common_dict , command = command , code = main .poll (), output = main_output , stdin = piped )))
138+ main = Popen (command , cwd = local_root , env = env , stdout = PIPE , stderr = PIPE if pipeto else STDOUT , stdin = null )
139+ if pipeto :
140+ pipeto (main .stdout )
141+ main_output = main .communicate ()[1 ].decode ('utf-8' ) # Might deadlock if stderr is written to a lot.
142+ else :
143+ main_output = main .communicate ()[0 ].decode ('utf-8' )
144+ log .debug (json .dumps (dict (cwd = local_root , command = command , code = main .poll (), output = main_output )))
154145
155146 # Verify success.
156- if piped and parent .poll () != 0 :
157- raise CalledProcessError (parent .poll (), piped , output = parent_output )
158147 if main .poll () != 0 :
159148 raise CalledProcessError (main .poll (), command , output = main_output )
160149
@@ -283,24 +272,36 @@ def export(local_root, commit, target):
283272 :param str target: Directory to export to.
284273 """
285274 log = logging .getLogger (__name__ )
286- git_command = ['git' , 'archive' , '--format=tar' , commit ]
287-
288- with TempDir () as temp_dir :
289- # Run commands.
290- run_command (local_root , ['tar' , '-x' , '-C' , temp_dir ], piped = git_command )
291-
292- # Copy to target. Overwrite existing but don't delete anything in target.
293- for s_dirpath , s_filenames in (i [::2 ] for i in os .walk (temp_dir ) if i [2 ]):
294- t_dirpath = os .path .join (target , os .path .relpath (s_dirpath , temp_dir ))
295- if not os .path .exists (t_dirpath ):
296- os .makedirs (t_dirpath )
297- for args in ((os .path .join (s_dirpath , f ), os .path .join (t_dirpath , f )) for f in s_filenames ):
298- try :
299- shutil .copy (* args )
300- except IOError :
301- if not os .path .islink (args [0 ]):
302- raise
303- log .debug ('Skipping broken symlink: %s' , args [0 ])
275+ target = os .path .realpath (target )
276+
277+ # Define extract function.
278+ def extract (stdout ):
279+ """Extract tar archive from "git archive" stdout.
280+
281+ :param file stdout: Handle to git's stdout pipe.
282+ """
283+ queued_links = list ()
284+ try :
285+ with tarfile .open (fileobj = stdout , mode = 'r|' ) as tar :
286+ for info in tar :
287+ log .debug ('name: %s; mode: %d; size: %s; type: %s' , info .name , info .mode , info .size , info .type )
288+ path = os .path .realpath (os .path .join (target , info .name ))
289+ if not path .startswith (target ): # Handle bad paths.
290+ log .warning ('Ignoring tar object path %s outside of target directory.' , info .name )
291+ elif info .isdir (): # Handle directories.
292+ if not os .path .exists (path ):
293+ os .makedirs (path , mode = info .mode )
294+ elif info .issym () or info .islnk (): # Queue links.
295+ queued_links .append (info )
296+ else : # Handle files.
297+ tar .extract (member = info , path = target )
298+ for info in (i for i in queued_links if os .path .exists (os .path .join (target , i .linkname ))):
299+ tar .extract (member = info , path = target )
300+ except tarfile .TarError as exc :
301+ log .debug ('Failed to extract output from "git archive" command: %s' , str (exc ))
302+
303+ # Run command.
304+ run_command (local_root , ['git' , 'archive' , '--format=tar' , commit ], pipeto = extract )
304305
305306
306307def clone (local_root , new_root , remote , branch , rel_dest , exclude ):
0 commit comments