Merge branch 'dk/p4-import-ctypes'
authorJunio C Hamano <gitster@pobox.com>
Mon, 26 Oct 2015 22:55:26 +0000 (15:55 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 26 Oct 2015 22:55:26 +0000 (15:55 -0700)
"git-p4" tried to use from ctypes module without first importing
it.

* dk/p4-import-ctypes:
git-p4: import the ctypes module

1  2 
git-p4.py
diff --combined git-p4.py
index daa60c60d6703ae76fd5cd168907f598207844d3,ff4113a661236f8306b0bd6ec1ced6600974c0ef..212ef2be9670bc2fe5573eee856471113eb96889
+++ b/git-p4.py
@@@ -22,8 -22,7 +22,9 @@@ import platfor
  import re
  import shutil
  import stat
 +import zipfile
 +import zlib
+ import ctypes
  
  try:
      from subprocess import CalledProcessError
@@@ -146,11 -145,13 +147,11 @@@ def read_pipe(c, ignore_error=False)
          sys.stderr.write('Reading pipe: %s\n' % str(c))
  
      expand = isinstance(c,basestring)
 -    p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
 -    pipe = p.stdout
 -    val = pipe.read()
 -    if p.wait() and not ignore_error:
 -        die('Command failed: %s' % str(c))
 -
 -    return val
 +    p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
 +    (out, err) = p.communicate()
 +    if p.returncode != 0 and not ignore_error:
 +        die('Command failed: %s\nError: %s' % (str(c), err))
 +    return out
  
  def p4_read_pipe(c, ignore_error=False):
      real_cmd = p4_build_cmd(c)
@@@ -932,182 -933,6 +933,182 @@@ def wildcard_present(path)
      m = re.search("[*#@%]", path)
      return m is not None
  
 +class LargeFileSystem(object):
 +    """Base class for large file system support."""
 +
 +    def __init__(self, writeToGitStream):
 +        self.largeFiles = set()
 +        self.writeToGitStream = writeToGitStream
 +
 +    def generatePointer(self, cloneDestination, contentFile):
 +        """Return the content of a pointer file that is stored in Git instead of
 +           the actual content."""
 +        assert False, "Method 'generatePointer' required in " + self.__class__.__name__
 +
 +    def pushFile(self, localLargeFile):
 +        """Push the actual content which is not stored in the Git repository to
 +           a server."""
 +        assert False, "Method 'pushFile' required in " + self.__class__.__name__
 +
 +    def hasLargeFileExtension(self, relPath):
 +        return reduce(
 +            lambda a, b: a or b,
 +            [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
 +            False
 +        )
 +
 +    def generateTempFile(self, contents):
 +        contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
 +        for d in contents:
 +            contentFile.write(d)
 +        contentFile.close()
 +        return contentFile.name
 +
 +    def exceedsLargeFileThreshold(self, relPath, contents):
 +        if gitConfigInt('git-p4.largeFileThreshold'):
 +            contentsSize = sum(len(d) for d in contents)
 +            if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
 +                return True
 +        if gitConfigInt('git-p4.largeFileCompressedThreshold'):
 +            contentsSize = sum(len(d) for d in contents)
 +            if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
 +                return False
 +            contentTempFile = self.generateTempFile(contents)
 +            compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
 +            zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
 +            zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
 +            zf.close()
 +            compressedContentsSize = zf.infolist()[0].compress_size
 +            os.remove(contentTempFile)
 +            os.remove(compressedContentFile.name)
 +            if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
 +                return True
 +        return False
 +
 +    def addLargeFile(self, relPath):
 +        self.largeFiles.add(relPath)
 +
 +    def removeLargeFile(self, relPath):
 +        self.largeFiles.remove(relPath)
 +
 +    def isLargeFile(self, relPath):
 +        return relPath in self.largeFiles
 +
 +    def processContent(self, git_mode, relPath, contents):
 +        """Processes the content of git fast import. This method decides if a
 +           file is stored in the large file system and handles all necessary
 +           steps."""
 +        if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
 +            contentTempFile = self.generateTempFile(contents)
 +            (git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
 +
 +            # Move temp file to final location in large file system
 +            largeFileDir = os.path.dirname(localLargeFile)
 +            if not os.path.isdir(largeFileDir):
 +                os.makedirs(largeFileDir)
 +            shutil.move(contentTempFile, localLargeFile)
 +            self.addLargeFile(relPath)
 +            if gitConfigBool('git-p4.largeFilePush'):
 +                self.pushFile(localLargeFile)
 +            if verbose:
 +                sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
 +        return (git_mode, contents)
 +
 +class MockLFS(LargeFileSystem):
 +    """Mock large file system for testing."""
 +
 +    def generatePointer(self, contentFile):
 +        """The pointer content is the original content prefixed with "pointer-".
 +           The local filename of the large file storage is derived from the file content.
 +           """
 +        with open(contentFile, 'r') as f:
 +            content = next(f)
 +            gitMode = '100644'
 +            pointerContents = 'pointer-' + content
 +            localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
 +            return (gitMode, pointerContents, localLargeFile)
 +
 +    def pushFile(self, localLargeFile):
 +        """The remote filename of the large file storage is the same as the local
 +           one but in a different directory.
 +           """
 +        remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
 +        if not os.path.exists(remotePath):
 +            os.makedirs(remotePath)
 +        shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
 +
 +class GitLFS(LargeFileSystem):
 +    """Git LFS as backend for the git-p4 large file system.
 +       See https://git-lfs.github.com/ for details."""
 +
 +    def __init__(self, *args):
 +        LargeFileSystem.__init__(self, *args)
 +        self.baseGitAttributes = []
 +
 +    def generatePointer(self, contentFile):
 +        """Generate a Git LFS pointer for the content. Return LFS Pointer file
 +           mode and content which is stored in the Git repository instead of
 +           the actual content. Return also the new location of the actual
 +           content.
 +           """
 +        pointerProcess = subprocess.Popen(
 +            ['git', 'lfs', 'pointer', '--file=' + contentFile],
 +            stdout=subprocess.PIPE
 +        )
 +        pointerFile = pointerProcess.stdout.read()
 +        if pointerProcess.wait():
 +            os.remove(contentFile)
 +            die('git-lfs pointer command failed. Did you install the extension?')
 +        pointerContents = [i+'\n' for i in pointerFile.split('\n')[2:][:-1]]
 +        oid = pointerContents[1].split(' ')[1].split(':')[1][:-1]
 +        localLargeFile = os.path.join(
 +            os.getcwd(),
 +            '.git', 'lfs', 'objects', oid[:2], oid[2:4],
 +            oid,
 +        )
 +        # LFS Spec states that pointer files should not have the executable bit set.
 +        gitMode = '100644'
 +        return (gitMode, pointerContents, localLargeFile)
 +
 +    def pushFile(self, localLargeFile):
 +        uploadProcess = subprocess.Popen(
 +            ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
 +        )
 +        if uploadProcess.wait():
 +            die('git-lfs push command failed. Did you define a remote?')
 +
 +    def generateGitAttributes(self):
 +        return (
 +            self.baseGitAttributes +
 +            [
 +                '\n',
 +                '#\n',
 +                '# Git LFS (see https://git-lfs.github.com/)\n',
 +                '#\n',
 +            ] +
 +            ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
 +                for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
 +            ] +
 +            ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
 +                for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
 +            ]
 +        )
 +
 +    def addLargeFile(self, relPath):
 +        LargeFileSystem.addLargeFile(self, relPath)
 +        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
 +
 +    def removeLargeFile(self, relPath):
 +        LargeFileSystem.removeLargeFile(self, relPath)
 +        self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
 +
 +    def processContent(self, git_mode, relPath, contents):
 +        if relPath == '.gitattributes':
 +            self.baseGitAttributes = contents
 +            return (git_mode, self.generateGitAttributes())
 +        else:
 +            return LargeFileSystem.processContent(self, git_mode, relPath, contents)
 +
  class Command:
      def __init__(self):
          self.usage = "usage: %prog [options]"
@@@ -1281,9 -1106,6 +1282,9 @@@ class P4Submit(Command, P4UserMap)
          self.p4HasMoveCommand = p4_has_move_command()
          self.branch = None
  
 +        if gitConfig('git-p4.largeFileSystem'):
 +            die("Large file system not supported for git-p4 submit command. Please remove it from config.")
 +
      def check(self):
          if len(p4CmdList("opened ...")) > 0:
              die("You have files opened with perforce! Close them before starting the sync.")
@@@ -2234,13 -2056,6 +2235,13 @@@ class P4Sync(Command, P4UserMap)
          self.clientSpecDirs = None
          self.tempBranches = []
          self.tempBranchLocation = "git-p4-tmp"
 +        self.largeFileSystem = None
 +
 +        if gitConfig('git-p4.largeFileSystem'):
 +            largeFileSystemConstructor = globals()[gitConfig('git-p4.largeFileSystem')]
 +            self.largeFileSystem = largeFileSystemConstructor(
 +                lambda git_mode, relPath, contents: self.writeToGitStream(git_mode, relPath, contents)
 +            )
  
          if gitConfig("git-p4.syncFromOrigin") == "false":
              self.syncWithOrigin = False
  
          return branches
  
 +    def writeToGitStream(self, gitMode, relPath, contents):
 +        self.gitStream.write('M %s inline %s\n' % (gitMode, relPath))
 +        self.gitStream.write('data %d\n' % sum(len(d) for d in contents))
 +        for d in contents:
 +            self.gitStream.write(d)
 +        self.gitStream.write('\n')
 +
      # output one file from the P4 stream
      # - helper for streamP4Files
  
              # them back too.  This is not needed to the cygwin windows version,
              # just the native "NT" type.
              #
 -            text = p4_read_pipe(['print', '-q', '-o', '-', "%s@%s" % (file['depotFile'], file['change']) ])
 -            if p4_version_string().find("/NT") >= 0:
 -                text = text.replace("\r\n", "\n")
 -            contents = [ text ]
 +            try:
 +                text = p4_read_pipe(['print', '-q', '-o', '-', '%s@%s' % (file['depotFile'], file['change'])])
 +            except Exception as e:
 +                if 'Translation of file content failed' in str(e):
 +                    type_base = 'binary'
 +                else:
 +                    raise e
 +            else:
 +                if p4_version_string().find('/NT') >= 0:
 +                    text = text.replace('\r\n', '\n')
 +                contents = [ text ]
  
          if type_base == "apple":
              # Apple filetype files will be streamed as a concatenation of
              text = regexp.sub(r'$\1$', text)
              contents = [ text ]
  
 -        self.gitStream.write("M %s inline %s\n" % (git_mode, relPath))
 +        try:
 +            relPath.decode('ascii')
 +        except:
 +            encoding = 'utf8'
 +            if gitConfig('git-p4.pathEncoding'):
 +                encoding = gitConfig('git-p4.pathEncoding')
 +            relPath = relPath.decode(encoding, 'replace').encode('utf8', 'replace')
 +            if self.verbose:
 +                print 'Path with non-ASCII characters detected. Used %s to encode: %s ' % (encoding, relPath)
  
 -        # total length...
 -        length = 0
 -        for d in contents:
 -            length = length + len(d)
 +        if self.largeFileSystem:
 +            (git_mode, contents) = self.largeFileSystem.processContent(git_mode, relPath, contents)
  
 -        self.gitStream.write("data %d\n" % length)
 -        for d in contents:
 -            self.gitStream.write(d)
 -        self.gitStream.write("\n")
 +        self.writeToGitStream(git_mode, relPath, contents)
  
      def streamOneP4Deletion(self, file):
          relPath = self.stripRepoPath(file['path'], self.branchPrefixes)
              sys.stdout.flush()
          self.gitStream.write("D %s\n" % relPath)
  
 +        if self.largeFileSystem and self.largeFileSystem.isLargeFile(relPath):
 +            self.largeFileSystem.removeLargeFile(relPath)
 +
      # handle another chunk of streaming data
      def streamP4FilesCb(self, marshalled):
  
          else:
              return "%s <a@b>" % userid
  
 -    # Stream a p4 tag
      def streamTag(self, gitStream, labelName, labelDetails, commit, epoch):
 +        """ Stream a p4 tag.
 +        commit is either a git commit, or a fast-import mark, ":<p4commit>"
 +        """
 +
          if verbose:
              print "writing tag %s for commit %s" % (labelName, commit)
          gitStream.write("tag %s\n" % labelName)
              self.clientSpecDirs.update_client_spec_path_cache(files)
  
          self.gitStream.write("commit %s\n" % branch)
 -#        gitStream.write("mark :%s\n" % details["change"])
 +        self.gitStream.write("mark :%s\n" % details["change"])
          self.committedChanges.add(int(details["change"]))
          committer = ""
          if author not in self.users:
              if change.has_key('change'):
                  # find the corresponding git commit; take the oldest commit
                  changelist = int(change['change'])
 -                gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
 -                     "--reverse", ":/\[git-p4:.*change = %d\]" % changelist])
 -                if len(gitCommit) == 0:
 -                    print "could not find git commit for changelist %d" % changelist
 -                else:
 -                    gitCommit = gitCommit.strip()
 +                if changelist in self.committedChanges:
 +                    gitCommit = ":%d" % changelist       # use a fast-import mark
                      commitFound = True
 +                else:
 +                    gitCommit = read_pipe(["git", "rev-list", "--max-count=1",
 +                        "--reverse", ":/\[git-p4:.*change = %d\]" % changelist], ignore_error=True)
 +                    if len(gitCommit) == 0:
 +                        print "importing label %s: could not find git commit for changelist %d" % (name, changelist)
 +                    else:
 +                        commitFound = True
 +                        gitCommit = gitCommit.strip()
 +
 +                if commitFound:
                      # Convert from p4 time format
                      try:
                          tmwhen = time.strptime(labelDetails['Update'], "%Y/%m/%d %H:%M:%S")