IntegHistory.py #1

  • //
  • guest/
  • ahmet_bilgin/
  • admin_toolkit/
  • IntegHistory.py
  • View
  • Commits
  • Open Download .zip Download (18 KB)
#!/usr/bin/env python

################################################################################
#
# Copyright (c) 2019, Perforce Software, Inc.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# DATE
#   
#   $Date: 2020/03/25 $
#
# SYNOPSIS
# 
#   IntegHistory.py [-v] [-o] [-p p4port] [-u p4user] [-c p4client] depotFile(s)[revRange]
#   * depotFile must be in depot syntax
#   * depotFile with spaces requires quote
#   * multiple depotFiles allowed
#   * wildcards and revision specifiers are supported
#
# DESCRIPTION
#
#   This script collects the full integration history of files.
#   The result is stored in a gzipped file (IntegHistory.log.gz) that can
#   be used by IntegHistoryRebuild.py to rebuild a partial Helix server.
#
# REQUIREMENT
#
# Command line p4 2013.2 or greater (check "p4 -V")
#
################################################################################

from __future__ import print_function
from collections import OrderedDict, deque
import subprocess
import argparse
import ast
import sys
import re
import gzip
import shutil
import os
import random
import string

python3 = sys.version_info[0] >= 3
minP4Version = 2013.2

class IntegHistory:
      def __init__(self, depotFiles, logName, verbose = False, obfuscate = False):
          self.depotFiles = depotFiles
          self.logName = logName
          self.caseHandling = b"insensitive"
          self.isSuper = False
          self.verbose = verbose
          self.obfuscate = obfuscate
          self.obfuscateDict = {}
          self.files = []
          self.integed = []
          self.depots = {}
          self.streams = []
          self.changes = {}
          self.depotRE = re.compile(b'\/\/(.*?)/.*')
          self.filelog1RE = re.compile(b'\.\.\. #(\d+) change (\d+)')
          self.filelog2RE = re.compile(b'\.\.\. \.\.\. .* (\/\/.*?)#')
          self.importRE = re.compile(b'... Paths\d+ import.* (.*)')
          try:
            self.log = open(logName, "w")
            # for Python 2 compatibility purpose
            self.output = getattr(self.log, 'buffer', self.log)
          except IOError:
            sys.exit("Error: can\'t create " + logName)
          try:
            self.output.write(b"['files', '")
            for i in range(len(self.depotFiles)):
                depotFile = self.depotFiles[i]
                if self.obfuscate:
                   depotFile = self.obfuscateFilepath(depotFile)
                self.output.write(depotFile)
                if i+1 < len(self.depotFiles):
                   self.output.write(b"', '")
            self.output.write(b"']\n")                    
          except IOError:
            sys.exit("Error: can\'t write in " + logName)
          match = re.match(b'.*\/(\d+.\d)\/.*',self.runCmd(b'p4 -V', False)[-1])
          if match and float(match.group(1)) < minP4Version:
            sys.exit("Error: minimum supported P4 version is " + str(minP4Version))
             
      def obfuscateSave(self, filename):
          with gzip.open(filename + ".gz", 'wb') as gzfile:
               for depotFile in self.files:
                   listStr = []
                   for str in depotFile.split('/'):
                       if not str == "":
                          listStr.append(self.obfuscateDict[str])
                       else:
                          listStr.append("")
                   gzfile.write(depotFile + " <=> " + '/'.join(listStr) + "\n")

      def obfuscateFilepath(self, filepath):
          strList = []
          for str in filepath.split('/'):
              obfuscatedStr = ""
              if not str == "":
                 if str in self.obfuscateDict:
                    obfuscatedStr = self.obfuscateDict[str]
                 else:
                    obfuscatedStr = ''.join(((random.choice(string.letters) if char.isalpha() else char) for char in str))
                    self.obfuscateDict[str] = obfuscatedStr
              strList.append(obfuscatedStr)
          return('/'.join(strList))

      def obfuscateOutput(self, line):
          match = re.match(b'(... (?:desc|Description) )(.*)', line)
          if match:
             line = match.group(1) + "obfuscated description\\n"
          else:
             match = re.match(b'(... (?:clientFile|clientRoot|clientCwd|serverRoot|Root|Value) )(.*)', line)
             if match:
                line = match.group(1) + "/obfuscated/path"
             else:
                match = re.match(b'(... (?:Depot|depotFile|lbrFile|StreamDepth|stream|Stream|Name|path\d+ .*?|toFile|fromFile|path|user.*|client.*|User|Client|Owner|Email|FullName|Host|View\d+|Host|Map) )(.*)', line)
                if match:
                   line = match.group(1) + self.obfuscateFilepath(match.group(2))
                else:
                   match = re.match(b'(... (?:parent|Parent|baseParent|parentBase) )(.*)', line)
                   if match and not match.group(2) == "none":
                      line = match.group(1) + self.obfuscateFilepath(match.group(2))
                   else:
                      match = re.match(b'(p4 .* (?:depot .*|stream .*|istat .*|integed) \")(.*)(\")', line)
                      if match:
                         line = match.group(1) + self.obfuscateFilepath(match.group(2)) + match.group(3)
                      else:
                         match = re.match(b'(p4 .* fstat .* \")(.*)(@.*\")', line)
                         if match:
                            line = match.group(1) + self.obfuscateFilepath(match.group(2)) + match.group(3)
          return(line)

      def runCmd(self, cmd, toLog = True):
          # for Python 2 compatibility purpose
          stderr = getattr(sys.stderr, 'buffer', sys.stderr)
          tagged = False
          if re.search(b'-ztag', cmd):
             tagged = True
          filtered = False
          if re.search(b'-ztag -F', cmd):
            filtered = True
          result = []
          if toLog:
             result.append(cmd)
          if sys.platform == 'win32':
             process = subprocess.Popen(cmd.decode(), shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
          else:
             process = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
          (output, err) = process.communicate()
          exitCode = process.wait()          
          if self.verbose or err:
             stderr.write(cmd + b"\n")
          if self.verbose:
             stderr.write(output)
          if err:
             stderr.write(b"Error: " + err)
          if b"Perforce password (P4PASSWD) invalid or unset." in err:
             sys.exit()
          if exitCode == 0 and output:
             if filtered or not tagged:
                result = result + list(output.splitlines())
             else:           
                out = deque(output.splitlines())
                line = out.popleft()
                block = line
                while out:
                      nextLine = out.popleft()
                      match = re.search(b'^\.\.\. ', nextLine)
                      while out and not match:
                            block = block + b"\\n" + nextLine
                            nextLine = out.popleft()
                            match = re.search(b'^\.\.\. ', nextLine)
                      result.append(block)
                      line = nextLine
                      block = line
             if toLog == True:
                if result:
                   self.output.write(b"[")
                   for i,item in enumerate(result):
                       if self.obfuscate:
                          item = self.obfuscateOutput(item)
                       self.output.write(b"'")
                       self.output.write(item.replace(b"\\",b"\\\\").replace(b"\\\\n",b"\\n").replace(b"'",b"\\'"))
                       self.output.write(b"'")
                       if i < len(result)-1:
                          self.output.write(b", ")
                   self.output.write(b"]\n")
          return(result)

      def getConfigData(self):
          for line in self.runCmd(b'p4 -ztag info'):
              match = re.search(b'\.\.\. caseHandling (.*)', line.strip())
              if match:
                 self.caseHandling = match.group(1)
                 break
          self.runCmd(b'p4 set')
          self.runCmd(b'p4 -ztag user -o')
          self.runCmd(b'p4 -ztag client -o')
          self.runCmd(b'p4 -ztag protects')
          if re.search(b'... permMax super', self.runCmd(b"p4 -ztag protects -m")[-1]):
             self.isSuper = True
             self.runCmd(b'p4 -ztag configure show')
          self.runCmd(b'p4 counters -e change')
          self.runCmd(b'p4 counters -e traits')

      def addParentStream(self, stream):
          stream = stream.replace(b"(",b"\(").replace(b")",b"\)").replace(b"&",b"\&")
          for parent in self.runCmd(b'p4 -F %Parent% streams -m1 -F Stream="' + stream + b'"', False):
              if not parent == b"none" and parent not in self.streams:
                 self.streams.append(parent)
                 self.addParentStream(parent)

      def addVirtualStream(self, stream):
          stream = stream.replace(b"(",b"\(").replace(b")",b"\)").replace(b"&",b"\&")
          for virtual in self.runCmd(b'p4 -F %Stream% streams -F "Parent=' + stream + b' & Type=virtual"', False):
              if virtual not in self.streams:
                 self.streams.append(virtual)
                 self.addVirtualStream(virtual)

      def getFilesHistory(self, depotFile):
          if (self.caseHandling == b"insensitive" and depotFile.lower() not in map(lambda name: name.lower(), self.files)) or \
             (self.caseHandling == b"sensitive" and depotFile not in self.files):
             filelog = self.runCmd(b'p4 filelog -1 -l -t "' + depotFile + b'"', False)
             if len(filelog) > 2:
                self.files.append(depotFile)
                self.parseDepotFile(depotFile)
                for line in filelog:
                    match = self.filelog1RE.match(line.strip())
                    if match:
                       if match.group(2) not in self.changes:
                          self.changes[match.group(2)] = []
                       self.changes[match.group(2)].append(depotFile + b"#" + match.group(1))
                    else:
                       match = self.filelog2RE.match(line.strip())
                       if match:
                          self.getFilesHistory(match.group(1))

      def parseDepotFile(self, depotFile):
          match = self.depotRE.match(depotFile)
          if match:
             depot = match.group(1)
             if depot not in self.depots:
                streamDepth = None
                result = self.runCmd(b'p4 -ztag -F "%Type% %StreamDepth%" depot -o "' + depot + b'"', False)
                if result:
                   if result[0][0:6] == b"stream":
                      if len(result[0]) > 7:
                         streamDepth = result[0][7:]
                      else:
                         # for release with stream depot and no StreamDepth field
                         streamDepth = b"//" + depot + b"/1"
                   self.depots[depot] = streamDepth
             else:
               streamDepth = self.depots[depot]
             if streamDepth:
                stream = b""
                nbSlash = 0
                for i, char in enumerate(depotFile):
                    if (not python3 and char == b'/') or (python3 and char == 47):
                       nbSlash += 1
                    if nbSlash > streamDepth.count(b'/'):
                       stream = depotFile[:i]
                       break
                stream = stream.replace(b"(",b"\(").replace(b")",b"\)").replace(b"&",b"\&")
                if stream not in self.streams and self.runCmd(b'p4 streams -F "Stream=' + stream + b'"', False):
                   self.streams.append(stream)
                   self.addVirtualStream(stream)
                   self.addParentStream(stream)

      def getDepotsData(self):
          for depot in self.depots:
              self.runCmd(b'p4 -ztag depot -o "' + depot + b'"')

      def getStreamsData(self):
          for stream in self.streams:
              spec = self.runCmd(b'p4 -ztag stream -o "' + stream + b'"')
              if "... Type virtual" not in spec:
                 self.runCmd(b'p4 -ztag istat -s "' + stream + b'"')
              for line in spec:
                  match = self.importRE.match(line)
                  if match:
                     self.parseDepotFile(match.group(1))

      def getFilesData(self):
          flags = b"-Ol -Oi" 
          if self.isSuper:
             flags = flags + b" -Oc"
          for change in self.changes:
              for depotFile in self.changes[change]:
                  if python3:
                     filename = bytearray(depotFile)[0:depotFile.find(b"#")]
                  else:
                     filename = depotFile[0:depotFile.find(b"#")]
                  self.runCmd(b'p4 -ztag fstat ' + flags + b' "' + filename + b'@=' + change + b'"')

      def getIntegedData(self):
          for change in self.changes:
              for depotFile in self.changes[change]:
                  if python3:
                     filename = bytearray(depotFile)[0:depotFile.find(b"#")]
                  else:
                     filename = depotFile[0:depotFile.find(b"#")]
                  for depotFile in self.runCmd(b'p4 -ztag -F%depotFile% files "' + filename + b'@=' + change + b'"', False):
                      if depotFile not in self.integed:
                         self.runCmd(b'p4 -ztag integed "' + depotFile + b'"')
                         self.integed.append(depotFile)
                       
      def getChangesData(self):
          for change in self.changes:
              self.runCmd(b'p4 -ztag changes -l @=' + change)

      def getData(self):
          self.getConfigData()
          for depotFile in self.depotFiles:
              for df in self.runCmd(b'p4 -ztag -F%depotFile% files "' + depotFile + b'"', False):
                  self.getFilesHistory(df)
          if len(self.files) == 0:
             sys.exit("Error: No file found, check " + self.logName)
          self.changes = OrderedDict(sorted(self.changes.items(), key=lambda t: t[0]))
          self.getStreamsData()
          self.getDepotsData()
          self.getFilesData()
          self.getIntegedData()
          self.getChangesData()
          self.log.close()
          if (self.obfuscate):
             self.obfuscateSave("IntegHistoryObfuscated.txt")
          try:
            with open(self.logName, 'rb') as uncompressed:
                 with gzip.open(self.logName + ".gz", 'wb') as compressed:
                      shutil.copyfileobj(uncompressed, compressed)
            if os.path.isfile(self.logName):
               os.remove(self.logName)
            return self.logName + ".gz"
          except:
            return self.logName
      
def setP4Env(p4port, p4user, p4client):
    output = subprocess.check_output(["p4", "set"], universal_newlines=True)
    for line in output.splitlines():
        match = re.search("P4PORT=(.*?) .*", line)
        if match:
           if not p4port:
              p4port = match.group(1)
        else:
            match = re.search("P4USER=(.*?) .*", line)
            if match:
               if not p4user:
                  p4user = match.group(1)
            else:
               match = re.search("P4CLIENT=(.*?) .*", line)
               if match:
                  if not p4client:
                     p4client = match.group(1)
    os.environ["P4CONFIG"] = ""
    if p4port:
       os.environ["P4PORT"] = p4port
    if p4user:
       os.environ["P4USER"] = p4user
    if p4client:
       os.environ["P4CLIENT"] = p4client

def main():
    parser = argparse.ArgumentParser(prog='IntegHistory', usage='%(prog)s [-v] [-o] [-P p4_path] [-p p4port] [-u p4user] [-c p4client] depotFiles(s)[revRange]')
    if python3:
       parser.add_argument("depotFiles", nargs="*", help="list of files in depot syntax separated with spaces", type=os.fsencode)
    else:
       parser.add_argument("depotFiles", nargs="*", help="list of files in depot syntax separated with spaces")
    parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true")
    parser.add_argument("-o", "--obfuscate", help="obfuscate command output", action="store_true")
    parser.add_argument("-P", "--path", dest="path", help="P4 path value")
    parser.add_argument("-p", "--p4port", dest="p4port", help="server value")
    parser.add_argument("-u", "--p4user", dest="p4user", help="user value")
    parser.add_argument("-c", "--p4client", dest="p4client", help="client value")
    args = parser.parse_args()
    if not args.depotFiles:
       sys.exit(parser.print_help())
    for depotFile in args.depotFiles:
        if not re.search(b'\/\/.*', depotFile):
           sys.exit(parser.print_help())
    if args.p4port or args.p4user or args.p4client:
       setP4Env(args.p4port, args.p4user, args.p4client)
    if args.path:
       os.environ["PATH"] = args.path + ":" + os.environ["PATH"]
    logName = os.path.abspath(os.getcwd() + "/IntegHistory.log")
    history = IntegHistory(args.depotFiles, logName, args.verbose, args.obfuscate)
    print("Info: " + history.getData() + " created successfully")
  
if __name__ == '__main__':
    main()
# Change User Description Committed
#1 26378 ahmet_bilgin Branching

//guest/perforce_software/admin_toolkit/...

to //guest/ahmet_bilgin/admin_toolkit/...
//guest/perforce_software/admin_toolkit/IntegHistory.py
#28 26280 Pascal Soccard Fixed missing byte string conversion
#27 26261 Pascal Soccard Fixed highest privilege level detection
#26 26260 Pascal Soccard Reworked errors sent to stderr due to differences between Python 2
and Python 3 on Windows
#25 26195 Pascal Soccard Retrieve depot/stream data for stream import/import+ mappings
#24 26113 Pascal Soccard Reworked how virtual stream spec is collected
Errors are now always reported (not just in verbose mode)
#23 25729 Pascal Soccard Fixed encoding issue specific to Windows
#22 25690 Pascal Soccard Fixed "p4 streams" filtering issue when a stream name includes the following
characters: ( ) &
These characters needs to be escaped in the filter string.
#21 25055 Pascal Soccard Added a new user data Obfuscate feature (-o flag)
An IntegHistoryObfuscated.txt.gz file is created which
list how the depot file names have been converted (to be kept by
the user running the IntegHistory.py scritp)
#20 24437 Pascal Soccard Fixed -v output issue for Python 3
#19 23437 Pascal Soccard Fixed stream identification problem affecting Windows
#18 23300 Pascal Soccard Fixed incorrect stream depth command
#17 22964 Pascal Soccard Added new options (verbosity and P4 path)
Fixed StreamDepth check for older version
Escaped \ for Windows path
#16 22883 Pascal Soccard More Windows and Python 2 vs Python 3 fixes
#15 22882 Pascal Soccard Reintroduced p4 version check and added fix for Popen; on Windows it
requires a string rather than a bytes which works fine on Linux
#14 22880 Pascal Soccard Reverted p4 version check as it was causing an issue with Python 3.5.3
#13 22878 Pascal Soccard Fixed super user detection again
#12 22877 Pascal Soccard Fixed Python 2.7 version check problem introduced by previous change
#11 22876 Pascal Soccard Added minimum p4 version check (>=2013.2)
#10 22867 Pascal Soccard Fixed check for super privileges detection
#9 22630 Pascal Soccard Fixed variable spelling mistake in a use case I have not tested after
previous change (multiple files arguments).
#8 22577 Pascal Soccard Using bytes rather than string, so the script can now deal properly
with non utf8 characters. IntegHistoryRebuild.py run with Python 3
will still fail when non utf8 characters are encountered. Use
Python 2.7 until I have figured out how to fix this issue.
#7 22469 Pascal Soccard Added support for task stream
Added support for undo actions
#6 22353 Pascal Soccard Fixed previous change; parent stream were not processed properly
#5 22323 Pascal Soccard Reworked script to handle lbr values for branched move/delete
db.rev records as "p4 fstat -Oc" does't provide these data.
Fixed use of stream depth which was not taking into account.
#4 22291 Pascal Soccard Added new P4 environment variables for command options
#3 19319 Pascal Soccard Moved recently added compressed code out of main(), so it can
be called by IntegHistoryRebuild.py
#2 19275 Pascal Soccard Changed compression method as it was causing some issues on Windows
#1 18490 Pascal Soccard This tool recreates a partial Helix server (metadata and dummy depot
files) using data collected from a Helix server using p4 commands.