PageRenderTime 59ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/git-p4.py

https://bitbucket.org/ssaasen/git
Python | 3705 lines | 3538 code | 87 blank | 80 comment | 190 complexity | 76f5696c7552338524af9f15837327c7 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, Apache-2.0, BSD-2-Clause

Large files files are truncated, but you can click here to view the full file

  1. #!/usr/bin/env python
  2. #
  3. # git-p4.py -- A tool for bidirectional operation between a Perforce depot and git.
  4. #
  5. # Author: Simon Hausmann <simon@lst.de>
  6. # Copyright: 2007 Simon Hausmann <simon@lst.de>
  7. # 2007 Trolltech ASA
  8. # License: MIT <http://www.opensource.org/licenses/mit-license.php>
  9. #
  10. import sys
  11. if sys.hexversion < 0x02040000:
  12. # The limiter is the subprocess module
  13. sys.stderr.write("git-p4: requires Python 2.4 or later.\n")
  14. sys.exit(1)
  15. import os
  16. import optparse
  17. import marshal
  18. import subprocess
  19. import tempfile
  20. import time
  21. import platform
  22. import re
  23. import shutil
  24. import stat
  25. import zipfile
  26. import zlib
  27. import ctypes
  28. try:
  29. from subprocess import CalledProcessError
  30. except ImportError:
  31. # from python2.7:subprocess.py
  32. # Exception classes used by this module.
  33. class CalledProcessError(Exception):
  34. """This exception is raised when a process run by check_call() returns
  35. a non-zero exit status. The exit status will be stored in the
  36. returncode attribute."""
  37. def __init__(self, returncode, cmd):
  38. self.returncode = returncode
  39. self.cmd = cmd
  40. def __str__(self):
  41. return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
  42. verbose = False
  43. # Only labels/tags matching this will be imported/exported
  44. defaultLabelRegexp = r'[a-zA-Z0-9_\-.]+$'
  45. # Grab changes in blocks of this many revisions, unless otherwise requested
  46. defaultBlockSize = 512
  47. def p4_build_cmd(cmd):
  48. """Build a suitable p4 command line.
  49. This consolidates building and returning a p4 command line into one
  50. location. It means that hooking into the environment, or other configuration
  51. can be done more easily.
  52. """
  53. real_cmd = ["p4"]
  54. user = gitConfig("git-p4.user")
  55. if len(user) > 0:
  56. real_cmd += ["-u",user]
  57. password = gitConfig("git-p4.password")
  58. if len(password) > 0:
  59. real_cmd += ["-P", password]
  60. port = gitConfig("git-p4.port")
  61. if len(port) > 0:
  62. real_cmd += ["-p", port]
  63. host = gitConfig("git-p4.host")
  64. if len(host) > 0:
  65. real_cmd += ["-H", host]
  66. client = gitConfig("git-p4.client")
  67. if len(client) > 0:
  68. real_cmd += ["-c", client]
  69. if isinstance(cmd,basestring):
  70. real_cmd = ' '.join(real_cmd) + ' ' + cmd
  71. else:
  72. real_cmd += cmd
  73. return real_cmd
  74. def chdir(path, is_client_path=False):
  75. """Do chdir to the given path, and set the PWD environment
  76. variable for use by P4. It does not look at getcwd() output.
  77. Since we're not using the shell, it is necessary to set the
  78. PWD environment variable explicitly.
  79. Normally, expand the path to force it to be absolute. This
  80. addresses the use of relative path names inside P4 settings,
  81. e.g. P4CONFIG=.p4config. P4 does not simply open the filename
  82. as given; it looks for .p4config using PWD.
  83. If is_client_path, the path was handed to us directly by p4,
  84. and may be a symbolic link. Do not call os.getcwd() in this
  85. case, because it will cause p4 to think that PWD is not inside
  86. the client path.
  87. """
  88. os.chdir(path)
  89. if not is_client_path:
  90. path = os.getcwd()
  91. os.environ['PWD'] = path
  92. def calcDiskFree():
  93. """Return free space in bytes on the disk of the given dirname."""
  94. if platform.system() == 'Windows':
  95. free_bytes = ctypes.c_ulonglong(0)
  96. ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.getcwd()), None, None, ctypes.pointer(free_bytes))
  97. return free_bytes.value
  98. else:
  99. st = os.statvfs(os.getcwd())
  100. return st.f_bavail * st.f_frsize
  101. def die(msg):
  102. if verbose:
  103. raise Exception(msg)
  104. else:
  105. sys.stderr.write(msg + "\n")
  106. sys.exit(1)
  107. def write_pipe(c, stdin):
  108. if verbose:
  109. sys.stderr.write('Writing pipe: %s\n' % str(c))
  110. expand = isinstance(c,basestring)
  111. p = subprocess.Popen(c, stdin=subprocess.PIPE, shell=expand)
  112. pipe = p.stdin
  113. val = pipe.write(stdin)
  114. pipe.close()
  115. if p.wait():
  116. die('Command failed: %s' % str(c))
  117. return val
  118. def p4_write_pipe(c, stdin):
  119. real_cmd = p4_build_cmd(c)
  120. return write_pipe(real_cmd, stdin)
  121. def read_pipe(c, ignore_error=False):
  122. if verbose:
  123. sys.stderr.write('Reading pipe: %s\n' % str(c))
  124. expand = isinstance(c,basestring)
  125. p = subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=expand)
  126. (out, err) = p.communicate()
  127. if p.returncode != 0 and not ignore_error:
  128. die('Command failed: %s\nError: %s' % (str(c), err))
  129. return out
  130. def p4_read_pipe(c, ignore_error=False):
  131. real_cmd = p4_build_cmd(c)
  132. return read_pipe(real_cmd, ignore_error)
  133. def read_pipe_lines(c):
  134. if verbose:
  135. sys.stderr.write('Reading pipe: %s\n' % str(c))
  136. expand = isinstance(c, basestring)
  137. p = subprocess.Popen(c, stdout=subprocess.PIPE, shell=expand)
  138. pipe = p.stdout
  139. val = pipe.readlines()
  140. if pipe.close() or p.wait():
  141. die('Command failed: %s' % str(c))
  142. return val
  143. def p4_read_pipe_lines(c):
  144. """Specifically invoke p4 on the command supplied. """
  145. real_cmd = p4_build_cmd(c)
  146. return read_pipe_lines(real_cmd)
  147. def p4_has_command(cmd):
  148. """Ask p4 for help on this command. If it returns an error, the
  149. command does not exist in this version of p4."""
  150. real_cmd = p4_build_cmd(["help", cmd])
  151. p = subprocess.Popen(real_cmd, stdout=subprocess.PIPE,
  152. stderr=subprocess.PIPE)
  153. p.communicate()
  154. return p.returncode == 0
  155. def p4_has_move_command():
  156. """See if the move command exists, that it supports -k, and that
  157. it has not been administratively disabled. The arguments
  158. must be correct, but the filenames do not have to exist. Use
  159. ones with wildcards so even if they exist, it will fail."""
  160. if not p4_has_command("move"):
  161. return False
  162. cmd = p4_build_cmd(["move", "-k", "@from", "@to"])
  163. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  164. (out, err) = p.communicate()
  165. # return code will be 1 in either case
  166. if err.find("Invalid option") >= 0:
  167. return False
  168. if err.find("disabled") >= 0:
  169. return False
  170. # assume it failed because @... was invalid changelist
  171. return True
  172. def system(cmd, ignore_error=False):
  173. expand = isinstance(cmd,basestring)
  174. if verbose:
  175. sys.stderr.write("executing %s\n" % str(cmd))
  176. retcode = subprocess.call(cmd, shell=expand)
  177. if retcode and not ignore_error:
  178. raise CalledProcessError(retcode, cmd)
  179. return retcode
  180. def p4_system(cmd):
  181. """Specifically invoke p4 as the system command. """
  182. real_cmd = p4_build_cmd(cmd)
  183. expand = isinstance(real_cmd, basestring)
  184. retcode = subprocess.call(real_cmd, shell=expand)
  185. if retcode:
  186. raise CalledProcessError(retcode, real_cmd)
  187. _p4_version_string = None
  188. def p4_version_string():
  189. """Read the version string, showing just the last line, which
  190. hopefully is the interesting version bit.
  191. $ p4 -V
  192. Perforce - The Fast Software Configuration Management System.
  193. Copyright 1995-2011 Perforce Software. All rights reserved.
  194. Rev. P4/NTX86/2011.1/393975 (2011/12/16).
  195. """
  196. global _p4_version_string
  197. if not _p4_version_string:
  198. a = p4_read_pipe_lines(["-V"])
  199. _p4_version_string = a[-1].rstrip()
  200. return _p4_version_string
  201. def p4_integrate(src, dest):
  202. p4_system(["integrate", "-Dt", wildcard_encode(src), wildcard_encode(dest)])
  203. def p4_sync(f, *options):
  204. p4_system(["sync"] + list(options) + [wildcard_encode(f)])
  205. def p4_add(f):
  206. # forcibly add file names with wildcards
  207. if wildcard_present(f):
  208. p4_system(["add", "-f", f])
  209. else:
  210. p4_system(["add", f])
  211. def p4_delete(f):
  212. p4_system(["delete", wildcard_encode(f)])
  213. def p4_edit(f, *options):
  214. p4_system(["edit"] + list(options) + [wildcard_encode(f)])
  215. def p4_revert(f):
  216. p4_system(["revert", wildcard_encode(f)])
  217. def p4_reopen(type, f):
  218. p4_system(["reopen", "-t", type, wildcard_encode(f)])
  219. def p4_move(src, dest):
  220. p4_system(["move", "-k", wildcard_encode(src), wildcard_encode(dest)])
  221. def p4_last_change():
  222. results = p4CmdList(["changes", "-m", "1"])
  223. return int(results[0]['change'])
  224. def p4_describe(change):
  225. """Make sure it returns a valid result by checking for
  226. the presence of field "time". Return a dict of the
  227. results."""
  228. ds = p4CmdList(["describe", "-s", str(change)])
  229. if len(ds) != 1:
  230. die("p4 describe -s %d did not return 1 result: %s" % (change, str(ds)))
  231. d = ds[0]
  232. if "p4ExitCode" in d:
  233. die("p4 describe -s %d exited with %d: %s" % (change, d["p4ExitCode"],
  234. str(d)))
  235. if "code" in d:
  236. if d["code"] == "error":
  237. die("p4 describe -s %d returned error code: %s" % (change, str(d)))
  238. if "time" not in d:
  239. die("p4 describe -s %d returned no \"time\": %s" % (change, str(d)))
  240. return d
  241. #
  242. # Canonicalize the p4 type and return a tuple of the
  243. # base type, plus any modifiers. See "p4 help filetypes"
  244. # for a list and explanation.
  245. #
  246. def split_p4_type(p4type):
  247. p4_filetypes_historical = {
  248. "ctempobj": "binary+Sw",
  249. "ctext": "text+C",
  250. "cxtext": "text+Cx",
  251. "ktext": "text+k",
  252. "kxtext": "text+kx",
  253. "ltext": "text+F",
  254. "tempobj": "binary+FSw",
  255. "ubinary": "binary+F",
  256. "uresource": "resource+F",
  257. "uxbinary": "binary+Fx",
  258. "xbinary": "binary+x",
  259. "xltext": "text+Fx",
  260. "xtempobj": "binary+Swx",
  261. "xtext": "text+x",
  262. "xunicode": "unicode+x",
  263. "xutf16": "utf16+x",
  264. }
  265. if p4type in p4_filetypes_historical:
  266. p4type = p4_filetypes_historical[p4type]
  267. mods = ""
  268. s = p4type.split("+")
  269. base = s[0]
  270. mods = ""
  271. if len(s) > 1:
  272. mods = s[1]
  273. return (base, mods)
  274. #
  275. # return the raw p4 type of a file (text, text+ko, etc)
  276. #
  277. def p4_type(f):
  278. results = p4CmdList(["fstat", "-T", "headType", wildcard_encode(f)])
  279. return results[0]['headType']
  280. #
  281. # Given a type base and modifier, return a regexp matching
  282. # the keywords that can be expanded in the file
  283. #
  284. def p4_keywords_regexp_for_type(base, type_mods):
  285. if base in ("text", "unicode", "binary"):
  286. kwords = None
  287. if "ko" in type_mods:
  288. kwords = 'Id|Header'
  289. elif "k" in type_mods:
  290. kwords = 'Id|Header|Author|Date|DateTime|Change|File|Revision'
  291. else:
  292. return None
  293. pattern = r"""
  294. \$ # Starts with a dollar, followed by...
  295. (%s) # one of the keywords, followed by...
  296. (:[^$\n]+)? # possibly an old expansion, followed by...
  297. \$ # another dollar
  298. """ % kwords
  299. return pattern
  300. else:
  301. return None
  302. #
  303. # Given a file, return a regexp matching the possible
  304. # RCS keywords that will be expanded, or None for files
  305. # with kw expansion turned off.
  306. #
  307. def p4_keywords_regexp_for_file(file):
  308. if not os.path.exists(file):
  309. return None
  310. else:
  311. (type_base, type_mods) = split_p4_type(p4_type(file))
  312. return p4_keywords_regexp_for_type(type_base, type_mods)
  313. def setP4ExecBit(file, mode):
  314. # Reopens an already open file and changes the execute bit to match
  315. # the execute bit setting in the passed in mode.
  316. p4Type = "+x"
  317. if not isModeExec(mode):
  318. p4Type = getP4OpenedType(file)
  319. p4Type = re.sub('^([cku]?)x(.*)', '\\1\\2', p4Type)
  320. p4Type = re.sub('(.*?\+.*?)x(.*?)', '\\1\\2', p4Type)
  321. if p4Type[-1] == "+":
  322. p4Type = p4Type[0:-1]
  323. p4_reopen(p4Type, file)
  324. def getP4OpenedType(file):
  325. # Returns the perforce file type for the given file.
  326. result = p4_read_pipe(["opened", wildcard_encode(file)])
  327. match = re.match(".*\((.+)\)( \*exclusive\*)?\r?$", result)
  328. if match:
  329. return match.group(1)
  330. else:
  331. die("Could not determine file type for %s (result: '%s')" % (file, result))
  332. # Return the set of all p4 labels
  333. def getP4Labels(depotPaths):
  334. labels = set()
  335. if isinstance(depotPaths,basestring):
  336. depotPaths = [depotPaths]
  337. for l in p4CmdList(["labels"] + ["%s..." % p for p in depotPaths]):
  338. label = l['label']
  339. labels.add(label)
  340. return labels
  341. # Return the set of all git tags
  342. def getGitTags():
  343. gitTags = set()
  344. for line in read_pipe_lines(["git", "tag"]):
  345. tag = line.strip()
  346. gitTags.add(tag)
  347. return gitTags
  348. def diffTreePattern():
  349. # This is a simple generator for the diff tree regex pattern. This could be
  350. # a class variable if this and parseDiffTreeEntry were a part of a class.
  351. pattern = re.compile(':(\d+) (\d+) (\w+) (\w+) ([A-Z])(\d+)?\t(.*?)((\t(.*))|$)')
  352. while True:
  353. yield pattern
  354. def parseDiffTreeEntry(entry):
  355. """Parses a single diff tree entry into its component elements.
  356. See git-diff-tree(1) manpage for details about the format of the diff
  357. output. This method returns a dictionary with the following elements:
  358. src_mode - The mode of the source file
  359. dst_mode - The mode of the destination file
  360. src_sha1 - The sha1 for the source file
  361. dst_sha1 - The sha1 fr the destination file
  362. status - The one letter status of the diff (i.e. 'A', 'M', 'D', etc)
  363. status_score - The score for the status (applicable for 'C' and 'R'
  364. statuses). This is None if there is no score.
  365. src - The path for the source file.
  366. dst - The path for the destination file. This is only present for
  367. copy or renames. If it is not present, this is None.
  368. If the pattern is not matched, None is returned."""
  369. match = diffTreePattern().next().match(entry)
  370. if match:
  371. return {
  372. 'src_mode': match.group(1),
  373. 'dst_mode': match.group(2),
  374. 'src_sha1': match.group(3),
  375. 'dst_sha1': match.group(4),
  376. 'status': match.group(5),
  377. 'status_score': match.group(6),
  378. 'src': match.group(7),
  379. 'dst': match.group(10)
  380. }
  381. return None
  382. def isModeExec(mode):
  383. # Returns True if the given git mode represents an executable file,
  384. # otherwise False.
  385. return mode[-3:] == "755"
  386. def isModeExecChanged(src_mode, dst_mode):
  387. return isModeExec(src_mode) != isModeExec(dst_mode)
  388. def p4CmdList(cmd, stdin=None, stdin_mode='w+b', cb=None):
  389. if isinstance(cmd,basestring):
  390. cmd = "-G " + cmd
  391. expand = True
  392. else:
  393. cmd = ["-G"] + cmd
  394. expand = False
  395. cmd = p4_build_cmd(cmd)
  396. if verbose:
  397. sys.stderr.write("Opening pipe: %s\n" % str(cmd))
  398. # Use a temporary file to avoid deadlocks without
  399. # subprocess.communicate(), which would put another copy
  400. # of stdout into memory.
  401. stdin_file = None
  402. if stdin is not None:
  403. stdin_file = tempfile.TemporaryFile(prefix='p4-stdin', mode=stdin_mode)
  404. if isinstance(stdin,basestring):
  405. stdin_file.write(stdin)
  406. else:
  407. for i in stdin:
  408. stdin_file.write(i + '\n')
  409. stdin_file.flush()
  410. stdin_file.seek(0)
  411. p4 = subprocess.Popen(cmd,
  412. shell=expand,
  413. stdin=stdin_file,
  414. stdout=subprocess.PIPE)
  415. result = []
  416. try:
  417. while True:
  418. entry = marshal.load(p4.stdout)
  419. if cb is not None:
  420. cb(entry)
  421. else:
  422. result.append(entry)
  423. except EOFError:
  424. pass
  425. exitCode = p4.wait()
  426. if exitCode != 0:
  427. entry = {}
  428. entry["p4ExitCode"] = exitCode
  429. result.append(entry)
  430. return result
  431. def p4Cmd(cmd):
  432. list = p4CmdList(cmd)
  433. result = {}
  434. for entry in list:
  435. result.update(entry)
  436. return result;
  437. def p4Where(depotPath):
  438. if not depotPath.endswith("/"):
  439. depotPath += "/"
  440. depotPathLong = depotPath + "..."
  441. outputList = p4CmdList(["where", depotPathLong])
  442. output = None
  443. for entry in outputList:
  444. if "depotFile" in entry:
  445. # Search for the base client side depot path, as long as it starts with the branch's P4 path.
  446. # The base path always ends with "/...".
  447. if entry["depotFile"].find(depotPath) == 0 and entry["depotFile"][-4:] == "/...":
  448. output = entry
  449. break
  450. elif "data" in entry:
  451. data = entry.get("data")
  452. space = data.find(" ")
  453. if data[:space] == depotPath:
  454. output = entry
  455. break
  456. if output == None:
  457. return ""
  458. if output["code"] == "error":
  459. return ""
  460. clientPath = ""
  461. if "path" in output:
  462. clientPath = output.get("path")
  463. elif "data" in output:
  464. data = output.get("data")
  465. lastSpace = data.rfind(" ")
  466. clientPath = data[lastSpace + 1:]
  467. if clientPath.endswith("..."):
  468. clientPath = clientPath[:-3]
  469. return clientPath
  470. def currentGitBranch():
  471. retcode = system(["git", "symbolic-ref", "-q", "HEAD"], ignore_error=True)
  472. if retcode != 0:
  473. # on a detached head
  474. return None
  475. else:
  476. return read_pipe(["git", "name-rev", "HEAD"]).split(" ")[1].strip()
  477. def isValidGitDir(path):
  478. if (os.path.exists(path + "/HEAD")
  479. and os.path.exists(path + "/refs") and os.path.exists(path + "/objects")):
  480. return True;
  481. return False
  482. def parseRevision(ref):
  483. return read_pipe("git rev-parse %s" % ref).strip()
  484. def branchExists(ref):
  485. rev = read_pipe(["git", "rev-parse", "-q", "--verify", ref],
  486. ignore_error=True)
  487. return len(rev) > 0
  488. def extractLogMessageFromGitCommit(commit):
  489. logMessage = ""
  490. ## fixme: title is first line of commit, not 1st paragraph.
  491. foundTitle = False
  492. for log in read_pipe_lines("git cat-file commit %s" % commit):
  493. if not foundTitle:
  494. if len(log) == 1:
  495. foundTitle = True
  496. continue
  497. logMessage += log
  498. return logMessage
  499. def extractSettingsGitLog(log):
  500. values = {}
  501. for line in log.split("\n"):
  502. line = line.strip()
  503. m = re.search (r"^ *\[git-p4: (.*)\]$", line)
  504. if not m:
  505. continue
  506. assignments = m.group(1).split (':')
  507. for a in assignments:
  508. vals = a.split ('=')
  509. key = vals[0].strip()
  510. val = ('='.join (vals[1:])).strip()
  511. if val.endswith ('\"') and val.startswith('"'):
  512. val = val[1:-1]
  513. values[key] = val
  514. paths = values.get("depot-paths")
  515. if not paths:
  516. paths = values.get("depot-path")
  517. if paths:
  518. values['depot-paths'] = paths.split(',')
  519. return values
  520. def gitBranchExists(branch):
  521. proc = subprocess.Popen(["git", "rev-parse", branch],
  522. stderr=subprocess.PIPE, stdout=subprocess.PIPE);
  523. return proc.wait() == 0;
  524. _gitConfig = {}
  525. def gitConfig(key, typeSpecifier=None):
  526. if not _gitConfig.has_key(key):
  527. cmd = [ "git", "config" ]
  528. if typeSpecifier:
  529. cmd += [ typeSpecifier ]
  530. cmd += [ key ]
  531. s = read_pipe(cmd, ignore_error=True)
  532. _gitConfig[key] = s.strip()
  533. return _gitConfig[key]
  534. def gitConfigBool(key):
  535. """Return a bool, using git config --bool. It is True only if the
  536. variable is set to true, and False if set to false or not present
  537. in the config."""
  538. if not _gitConfig.has_key(key):
  539. _gitConfig[key] = gitConfig(key, '--bool') == "true"
  540. return _gitConfig[key]
  541. def gitConfigInt(key):
  542. if not _gitConfig.has_key(key):
  543. cmd = [ "git", "config", "--int", key ]
  544. s = read_pipe(cmd, ignore_error=True)
  545. v = s.strip()
  546. try:
  547. _gitConfig[key] = int(gitConfig(key, '--int'))
  548. except ValueError:
  549. _gitConfig[key] = None
  550. return _gitConfig[key]
  551. def gitConfigList(key):
  552. if not _gitConfig.has_key(key):
  553. s = read_pipe(["git", "config", "--get-all", key], ignore_error=True)
  554. _gitConfig[key] = s.strip().split(os.linesep)
  555. if _gitConfig[key] == ['']:
  556. _gitConfig[key] = []
  557. return _gitConfig[key]
  558. def p4BranchesInGit(branchesAreInRemotes=True):
  559. """Find all the branches whose names start with "p4/", looking
  560. in remotes or heads as specified by the argument. Return
  561. a dictionary of { branch: revision } for each one found.
  562. The branch names are the short names, without any
  563. "p4/" prefix."""
  564. branches = {}
  565. cmdline = "git rev-parse --symbolic "
  566. if branchesAreInRemotes:
  567. cmdline += "--remotes"
  568. else:
  569. cmdline += "--branches"
  570. for line in read_pipe_lines(cmdline):
  571. line = line.strip()
  572. # only import to p4/
  573. if not line.startswith('p4/'):
  574. continue
  575. # special symbolic ref to p4/master
  576. if line == "p4/HEAD":
  577. continue
  578. # strip off p4/ prefix
  579. branch = line[len("p4/"):]
  580. branches[branch] = parseRevision(line)
  581. return branches
  582. def branch_exists(branch):
  583. """Make sure that the given ref name really exists."""
  584. cmd = [ "git", "rev-parse", "--symbolic", "--verify", branch ]
  585. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  586. out, _ = p.communicate()
  587. if p.returncode:
  588. return False
  589. # expect exactly one line of output: the branch name
  590. return out.rstrip() == branch
  591. def findUpstreamBranchPoint(head = "HEAD"):
  592. branches = p4BranchesInGit()
  593. # map from depot-path to branch name
  594. branchByDepotPath = {}
  595. for branch in branches.keys():
  596. tip = branches[branch]
  597. log = extractLogMessageFromGitCommit(tip)
  598. settings = extractSettingsGitLog(log)
  599. if settings.has_key("depot-paths"):
  600. paths = ",".join(settings["depot-paths"])
  601. branchByDepotPath[paths] = "remotes/p4/" + branch
  602. settings = None
  603. parent = 0
  604. while parent < 65535:
  605. commit = head + "~%s" % parent
  606. log = extractLogMessageFromGitCommit(commit)
  607. settings = extractSettingsGitLog(log)
  608. if settings.has_key("depot-paths"):
  609. paths = ",".join(settings["depot-paths"])
  610. if branchByDepotPath.has_key(paths):
  611. return [branchByDepotPath[paths], settings]
  612. parent = parent + 1
  613. return ["", settings]
  614. def createOrUpdateBranchesFromOrigin(localRefPrefix = "refs/remotes/p4/", silent=True):
  615. if not silent:
  616. print ("Creating/updating branch(es) in %s based on origin branch(es)"
  617. % localRefPrefix)
  618. originPrefix = "origin/p4/"
  619. for line in read_pipe_lines("git rev-parse --symbolic --remotes"):
  620. line = line.strip()
  621. if (not line.startswith(originPrefix)) or line.endswith("HEAD"):
  622. continue
  623. headName = line[len(originPrefix):]
  624. remoteHead = localRefPrefix + headName
  625. originHead = line
  626. original = extractSettingsGitLog(extractLogMessageFromGitCommit(originHead))
  627. if (not original.has_key('depot-paths')
  628. or not original.has_key('change')):
  629. continue
  630. update = False
  631. if not gitBranchExists(remoteHead):
  632. if verbose:
  633. print "creating %s" % remoteHead
  634. update = True
  635. else:
  636. settings = extractSettingsGitLog(extractLogMessageFromGitCommit(remoteHead))
  637. if settings.has_key('change') > 0:
  638. if settings['depot-paths'] == original['depot-paths']:
  639. originP4Change = int(original['change'])
  640. p4Change = int(settings['change'])
  641. if originP4Change > p4Change:
  642. print ("%s (%s) is newer than %s (%s). "
  643. "Updating p4 branch from origin."
  644. % (originHead, originP4Change,
  645. remoteHead, p4Change))
  646. update = True
  647. else:
  648. print ("Ignoring: %s was imported from %s while "
  649. "%s was imported from %s"
  650. % (originHead, ','.join(original['depot-paths']),
  651. remoteHead, ','.join(settings['depot-paths'])))
  652. if update:
  653. system("git update-ref %s %s" % (remoteHead, originHead))
  654. def originP4BranchesExist():
  655. return gitBranchExists("origin") or gitBranchExists("origin/p4") or gitBranchExists("origin/p4/master")
  656. def p4ParseNumericChangeRange(parts):
  657. changeStart = int(parts[0][1:])
  658. if parts[1] == '#head':
  659. changeEnd = p4_last_change()
  660. else:
  661. changeEnd = int(parts[1])
  662. return (changeStart, changeEnd)
  663. def chooseBlockSize(blockSize):
  664. if blockSize:
  665. return blockSize
  666. else:
  667. return defaultBlockSize
  668. def p4ChangesForPaths(depotPaths, changeRange, requestedBlockSize):
  669. assert depotPaths
  670. # Parse the change range into start and end. Try to find integer
  671. # revision ranges as these can be broken up into blocks to avoid
  672. # hitting server-side limits (maxrows, maxscanresults). But if
  673. # that doesn't work, fall back to using the raw revision specifier
  674. # strings, without using block mode.
  675. if changeRange is None or changeRange == '':
  676. changeStart = 1
  677. changeEnd = p4_last_change()
  678. block_size = chooseBlockSize(requestedBlockSize)
  679. else:
  680. parts = changeRange.split(',')
  681. assert len(parts) == 2
  682. try:
  683. (changeStart, changeEnd) = p4ParseNumericChangeRange(parts)
  684. block_size = chooseBlockSize(requestedBlockSize)
  685. except:
  686. changeStart = parts[0][1:]
  687. changeEnd = parts[1]
  688. if requestedBlockSize:
  689. die("cannot use --changes-block-size with non-numeric revisions")
  690. block_size = None
  691. changes = []
  692. # Retrieve changes a block at a time, to prevent running
  693. # into a MaxResults/MaxScanRows error from the server.
  694. while True:
  695. cmd = ['changes']
  696. if block_size:
  697. end = min(changeEnd, changeStart + block_size)
  698. revisionRange = "%d,%d" % (changeStart, end)
  699. else:
  700. revisionRange = "%s,%s" % (changeStart, changeEnd)
  701. for p in depotPaths:
  702. cmd += ["%s...@%s" % (p, revisionRange)]
  703. # Insert changes in chronological order
  704. for line in reversed(p4_read_pipe_lines(cmd)):
  705. changes.append(int(line.split(" ")[1]))
  706. if not block_size:
  707. break
  708. if end >= changeEnd:
  709. break
  710. changeStart = end + 1
  711. changes = sorted(changes)
  712. return changes
  713. def p4PathStartsWith(path, prefix):
  714. # This method tries to remedy a potential mixed-case issue:
  715. #
  716. # If UserA adds //depot/DirA/file1
  717. # and UserB adds //depot/dira/file2
  718. #
  719. # we may or may not have a problem. If you have core.ignorecase=true,
  720. # we treat DirA and dira as the same directory
  721. if gitConfigBool("core.ignorecase"):
  722. return path.lower().startswith(prefix.lower())
  723. return path.startswith(prefix)
  724. def getClientSpec():
  725. """Look at the p4 client spec, create a View() object that contains
  726. all the mappings, and return it."""
  727. specList = p4CmdList("client -o")
  728. if len(specList) != 1:
  729. die('Output from "client -o" is %d lines, expecting 1' %
  730. len(specList))
  731. # dictionary of all client parameters
  732. entry = specList[0]
  733. # the //client/ name
  734. client_name = entry["Client"]
  735. # just the keys that start with "View"
  736. view_keys = [ k for k in entry.keys() if k.startswith("View") ]
  737. # hold this new View
  738. view = View(client_name)
  739. # append the lines, in order, to the view
  740. for view_num in range(len(view_keys)):
  741. k = "View%d" % view_num
  742. if k not in view_keys:
  743. die("Expected view key %s missing" % k)
  744. view.append(entry[k])
  745. return view
  746. def getClientRoot():
  747. """Grab the client directory."""
  748. output = p4CmdList("client -o")
  749. if len(output) != 1:
  750. die('Output from "client -o" is %d lines, expecting 1' % len(output))
  751. entry = output[0]
  752. if "Root" not in entry:
  753. die('Client has no "Root"')
  754. return entry["Root"]
  755. #
  756. # P4 wildcards are not allowed in filenames. P4 complains
  757. # if you simply add them, but you can force it with "-f", in
  758. # which case it translates them into %xx encoding internally.
  759. #
  760. def wildcard_decode(path):
  761. # Search for and fix just these four characters. Do % last so
  762. # that fixing it does not inadvertently create new %-escapes.
  763. # Cannot have * in a filename in windows; untested as to
  764. # what p4 would do in such a case.
  765. if not platform.system() == "Windows":
  766. path = path.replace("%2A", "*")
  767. path = path.replace("%23", "#") \
  768. .replace("%40", "@") \
  769. .replace("%25", "%")
  770. return path
  771. def wildcard_encode(path):
  772. # do % first to avoid double-encoding the %s introduced here
  773. path = path.replace("%", "%25") \
  774. .replace("*", "%2A") \
  775. .replace("#", "%23") \
  776. .replace("@", "%40")
  777. return path
  778. def wildcard_present(path):
  779. m = re.search("[*#@%]", path)
  780. return m is not None
  781. class LargeFileSystem(object):
  782. """Base class for large file system support."""
  783. def __init__(self, writeToGitStream):
  784. self.largeFiles = set()
  785. self.writeToGitStream = writeToGitStream
  786. def generatePointer(self, cloneDestination, contentFile):
  787. """Return the content of a pointer file that is stored in Git instead of
  788. the actual content."""
  789. assert False, "Method 'generatePointer' required in " + self.__class__.__name__
  790. def pushFile(self, localLargeFile):
  791. """Push the actual content which is not stored in the Git repository to
  792. a server."""
  793. assert False, "Method 'pushFile' required in " + self.__class__.__name__
  794. def hasLargeFileExtension(self, relPath):
  795. return reduce(
  796. lambda a, b: a or b,
  797. [relPath.endswith('.' + e) for e in gitConfigList('git-p4.largeFileExtensions')],
  798. False
  799. )
  800. def generateTempFile(self, contents):
  801. contentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
  802. for d in contents:
  803. contentFile.write(d)
  804. contentFile.close()
  805. return contentFile.name
  806. def exceedsLargeFileThreshold(self, relPath, contents):
  807. if gitConfigInt('git-p4.largeFileThreshold'):
  808. contentsSize = sum(len(d) for d in contents)
  809. if contentsSize > gitConfigInt('git-p4.largeFileThreshold'):
  810. return True
  811. if gitConfigInt('git-p4.largeFileCompressedThreshold'):
  812. contentsSize = sum(len(d) for d in contents)
  813. if contentsSize <= gitConfigInt('git-p4.largeFileCompressedThreshold'):
  814. return False
  815. contentTempFile = self.generateTempFile(contents)
  816. compressedContentFile = tempfile.NamedTemporaryFile(prefix='git-p4-large-file', delete=False)
  817. zf = zipfile.ZipFile(compressedContentFile.name, mode='w')
  818. zf.write(contentTempFile, compress_type=zipfile.ZIP_DEFLATED)
  819. zf.close()
  820. compressedContentsSize = zf.infolist()[0].compress_size
  821. os.remove(contentTempFile)
  822. os.remove(compressedContentFile.name)
  823. if compressedContentsSize > gitConfigInt('git-p4.largeFileCompressedThreshold'):
  824. return True
  825. return False
  826. def addLargeFile(self, relPath):
  827. self.largeFiles.add(relPath)
  828. def removeLargeFile(self, relPath):
  829. self.largeFiles.remove(relPath)
  830. def isLargeFile(self, relPath):
  831. return relPath in self.largeFiles
  832. def processContent(self, git_mode, relPath, contents):
  833. """Processes the content of git fast import. This method decides if a
  834. file is stored in the large file system and handles all necessary
  835. steps."""
  836. if self.exceedsLargeFileThreshold(relPath, contents) or self.hasLargeFileExtension(relPath):
  837. contentTempFile = self.generateTempFile(contents)
  838. (git_mode, contents, localLargeFile) = self.generatePointer(contentTempFile)
  839. # Move temp file to final location in large file system
  840. largeFileDir = os.path.dirname(localLargeFile)
  841. if not os.path.isdir(largeFileDir):
  842. os.makedirs(largeFileDir)
  843. shutil.move(contentTempFile, localLargeFile)
  844. self.addLargeFile(relPath)
  845. if gitConfigBool('git-p4.largeFilePush'):
  846. self.pushFile(localLargeFile)
  847. if verbose:
  848. sys.stderr.write("%s moved to large file system (%s)\n" % (relPath, localLargeFile))
  849. return (git_mode, contents)
  850. class MockLFS(LargeFileSystem):
  851. """Mock large file system for testing."""
  852. def generatePointer(self, contentFile):
  853. """The pointer content is the original content prefixed with "pointer-".
  854. The local filename of the large file storage is derived from the file content.
  855. """
  856. with open(contentFile, 'r') as f:
  857. content = next(f)
  858. gitMode = '100644'
  859. pointerContents = 'pointer-' + content
  860. localLargeFile = os.path.join(os.getcwd(), '.git', 'mock-storage', 'local', content[:-1])
  861. return (gitMode, pointerContents, localLargeFile)
  862. def pushFile(self, localLargeFile):
  863. """The remote filename of the large file storage is the same as the local
  864. one but in a different directory.
  865. """
  866. remotePath = os.path.join(os.path.dirname(localLargeFile), '..', 'remote')
  867. if not os.path.exists(remotePath):
  868. os.makedirs(remotePath)
  869. shutil.copyfile(localLargeFile, os.path.join(remotePath, os.path.basename(localLargeFile)))
  870. class GitLFS(LargeFileSystem):
  871. """Git LFS as backend for the git-p4 large file system.
  872. See https://git-lfs.github.com/ for details."""
  873. def __init__(self, *args):
  874. LargeFileSystem.__init__(self, *args)
  875. self.baseGitAttributes = []
  876. def generatePointer(self, contentFile):
  877. """Generate a Git LFS pointer for the content. Return LFS Pointer file
  878. mode and content which is stored in the Git repository instead of
  879. the actual content. Return also the new location of the actual
  880. content.
  881. """
  882. pointerProcess = subprocess.Popen(
  883. ['git', 'lfs', 'pointer', '--file=' + contentFile],
  884. stdout=subprocess.PIPE
  885. )
  886. pointerFile = pointerProcess.stdout.read()
  887. if pointerProcess.wait():
  888. os.remove(contentFile)
  889. die('git-lfs pointer command failed. Did you install the extension?')
  890. # Git LFS removed the preamble in the output of the 'pointer' command
  891. # starting from version 1.2.0. Check for the preamble here to support
  892. # earlier versions.
  893. # c.f. https://github.com/github/git-lfs/commit/da2935d9a739592bc775c98d8ef4df9c72ea3b43
  894. if pointerFile.startswith('Git LFS pointer for'):
  895. pointerFile = re.sub(r'Git LFS pointer for.*\n\n', '', pointerFile)
  896. oid = re.search(r'^oid \w+:(\w+)', pointerFile, re.MULTILINE).group(1)
  897. localLargeFile = os.path.join(
  898. os.getcwd(),
  899. '.git', 'lfs', 'objects', oid[:2], oid[2:4],
  900. oid,
  901. )
  902. # LFS Spec states that pointer files should not have the executable bit set.
  903. gitMode = '100644'
  904. return (gitMode, pointerFile, localLargeFile)
  905. def pushFile(self, localLargeFile):
  906. uploadProcess = subprocess.Popen(
  907. ['git', 'lfs', 'push', '--object-id', 'origin', os.path.basename(localLargeFile)]
  908. )
  909. if uploadProcess.wait():
  910. die('git-lfs push command failed. Did you define a remote?')
  911. def generateGitAttributes(self):
  912. return (
  913. self.baseGitAttributes +
  914. [
  915. '\n',
  916. '#\n',
  917. '# Git LFS (see https://git-lfs.github.com/)\n',
  918. '#\n',
  919. ] +
  920. ['*.' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
  921. for f in sorted(gitConfigList('git-p4.largeFileExtensions'))
  922. ] +
  923. ['/' + f.replace(' ', '[[:space:]]') + ' filter=lfs -text\n'
  924. for f in sorted(self.largeFiles) if not self.hasLargeFileExtension(f)
  925. ]
  926. )
  927. def addLargeFile(self, relPath):
  928. LargeFileSystem.addLargeFile(self, relPath)
  929. self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
  930. def removeLargeFile(self, relPath):
  931. LargeFileSystem.removeLargeFile(self, relPath)
  932. self.writeToGitStream('100644', '.gitattributes', self.generateGitAttributes())
  933. def processContent(self, git_mode, relPath, contents):
  934. if relPath == '.gitattributes':
  935. self.baseGitAttributes = contents
  936. return (git_mode, self.generateGitAttributes())
  937. else:
  938. return LargeFileSystem.processContent(self, git_mode, relPath, contents)
  939. class Command:
  940. def __init__(self):
  941. self.usage = "usage: %prog [options]"
  942. self.needsGit = True
  943. self.verbose = False
  944. class P4UserMap:
  945. def __init__(self):
  946. self.userMapFromPerforceServer = False
  947. self.myP4UserId = None
  948. def p4UserId(self):
  949. if self.myP4UserId:
  950. return self.myP4UserId
  951. results = p4CmdList("user -o")
  952. for r in results:
  953. if r.has_key('User'):
  954. self.myP4UserId = r['User']
  955. return r['User']
  956. die("Could not find your p4 user id")
  957. def p4UserIsMe(self, p4User):
  958. # return True if the given p4 user is actually me
  959. me = self.p4UserId()
  960. if not p4User or p4User != me:
  961. return False
  962. else:
  963. return True
  964. def getUserCacheFilename(self):
  965. home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
  966. return home + "/.gitp4-usercache.txt"
  967. def getUserMapFromPerforceServer(self):
  968. if self.userMapFromPerforceServer:
  969. return
  970. self.users = {}
  971. self.emails = {}
  972. for output in p4CmdList("users"):
  973. if not output.has_key("User"):
  974. continue
  975. self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
  976. self.emails[output["Email"]] = output["User"]
  977. mapUserConfigRegex = re.compile(r"^\s*(\S+)\s*=\s*(.+)\s*<(\S+)>\s*$", re.VERBOSE)
  978. for mapUserConfig in gitConfigList("git-p4.mapUser"):
  979. mapUser = mapUserConfigRegex.findall(mapUserConfig)
  980. if mapUser and len(mapUser[0]) == 3:
  981. user = mapUser[0][0]
  982. fullname = mapUser[0][1]
  983. email = mapUser[0][2]
  984. self.users[user] = fullname + " <" + email + ">"
  985. self.emails[email] = user
  986. s = ''
  987. for (key, val) in self.users.items():
  988. s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
  989. open(self.getUserCacheFilename(), "wb").write(s)
  990. self.userMapFromPerforceServer = True
  991. def loadUserMapFromCache(self):
  992. self.users = {}
  993. self.userMapFromPerforceServer = False
  994. try:
  995. cache = open(self.getUserCacheFilename(), "rb")
  996. lines = cache.readlines()
  997. cache.close()
  998. for line in lines:
  999. entry = line.strip().split("\t")
  1000. self.users[entry[0]] = entry[1]
  1001. except IOError:
  1002. self.getUserMapFromPerforceServer()
  1003. class P4Debug(Command):
  1004. def __init__(self):
  1005. Command.__init__(self)
  1006. self.options = []
  1007. self.description = "A tool to debug the output of p4 -G."
  1008. self.needsGit = False
  1009. def run(self, args):
  1010. j = 0
  1011. for output in p4CmdList(args):
  1012. print 'Element: %d' % j
  1013. j += 1
  1014. print output
  1015. return True
  1016. class P4RollBack(Command):
  1017. def __init__(self):
  1018. Command.__init__(self)
  1019. self.options = [
  1020. optparse.make_option("--local", dest="rollbackLocalBranches", action="store_true")
  1021. ]
  1022. self.description = "A tool to debug the multi-branch import. Don't use :)"
  1023. self.rollbackLocalBranches = False
  1024. def run(self, args):
  1025. if len(args) != 1:
  1026. return False
  1027. maxChange = int(args[0])
  1028. if "p4ExitCode" in p4Cmd("changes -m 1"):
  1029. die("Problems executing p4");
  1030. if self.rollbackLocalBranches:
  1031. refPrefix = "refs/heads/"
  1032. lines = read_pipe_lines("git rev-parse --symbolic --branches")
  1033. else:
  1034. refPrefix = "refs/remotes/"
  1035. lines = read_pipe_lines("git rev-parse --symbolic --remotes")
  1036. for line in lines:
  1037. if self.rollbackLocalBranches or (line.startswith("p4/") and line != "p4/HEAD\n"):
  1038. line = line.strip()
  1039. ref = refPrefix + line
  1040. log = extractLogMessageFromGitCommit(ref)
  1041. settings = extractSettingsGitLog(log)
  1042. depotPaths = settings['depot-paths']
  1043. change = settings['change']
  1044. changed = False
  1045. if len(p4Cmd("changes -m 1 " + ' '.join (['%s...@%s' % (p, maxChange)
  1046. for p in depotPaths]))) == 0:
  1047. print "Branch %s did not exist at change %s, deleting." % (ref, maxChange)
  1048. system("git update-ref -d %s `git rev-parse %s`" % (ref, ref))
  1049. continue
  1050. while change and int(change) > maxChange:
  1051. changed = True
  1052. if self.verbose:
  1053. print "%s is at %s ; rewinding towards %s" % (ref, change, maxChange)
  1054. system("git update-ref %s \"%s^\"" % (ref, ref))
  1055. log = extractLogMessageFromGitCommit(ref)
  1056. settings = extractSettingsGitLog(log)
  1057. depotPaths = settings['depot-paths']
  1058. change = settings['change']
  1059. if changed:
  1060. print "%s rewound to %s" % (ref, change)
  1061. return True
  1062. class P4Submit(Command, P4UserMap):
  1063. conflict_behavior_choices = ("ask", "skip", "quit")
  1064. def __init__(self):
  1065. Command.__init__(self)
  1066. P4UserMap.__init__(self)
  1067. self.options = [
  1068. optparse.make_option("--origin", dest="origin"),
  1069. optparse.make_option("-M", dest="detectRenames", action="store_true"),
  1070. # preserve the user, requires relevant p4 permissions
  1071. optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
  1072. optparse.make_option("--export-labels", dest="exportLabels", action="store_true"),
  1073. optparse.make_option("--dry-run", "-n", dest="dry_run", action="store_true"),
  1074. optparse.make_option("--prepare-p4-only", dest="prepare_p4_only", action="store_true"),
  1075. optparse.make_option("--conflict", dest="conflict_behavior",
  1076. choices=self.conflict_behavior_choices),
  1077. optparse.make_option("--branch", dest="branch"),
  1078. ]
  1079. self.description = "Submit changes from git to the perforce depot."
  1080. self.usage += " [name of git branch to submit into perforce depot]"
  1081. self.origin = ""
  1082. self.detectRenames = False
  1083. self.preserveUser = gitConfigBool("git-p4.preserveUser")
  1084. self.dry_run = False
  1085. self.prepare_p4_only = False
  1086. self.conflict_behavior = None
  1087. self.isWindows = (platform.system() == "Windows")
  1088. self.exportLabels = False
  1089. self.p4HasMoveCommand = p4_has_move_command()
  1090. self.branch = None
  1091. if gitConfig('git-p4.largeFileSystem'):
  1092. die("Large file system not supported for git-p4 submit command. Please remove it from config.")
  1093. def check(self):
  1094. if len(p4CmdList("opened ...")) > 0:
  1095. die("You have files opened with perforce! Close them before starting the sync.")
  1096. def separate_jobs_from_description(self, message):
  1097. """Extract and return a possible Jobs field in the commit
  1098. message. It goes into a separate section in the p4 change
  1099. specification.
  1100. A jobs line starts with "Jobs:" and looks like a new field
  1101. in a form. Values are white-space separated on the same
  1102. line or on following lines that start with a tab.
  1103. This does not parse and extract the full git commit message
  1104. like a p4 form. It just sees the Jobs: line as a marker
  1105. to pass everything from then on directly into the p4 form,
  1106. but outside the description section.
  1107. Return a tuple (stripped log message, jobs string)."""
  1108. m = re.search(r'^Jobs:', message, re.MULTILINE)
  1109. if m is None:
  1110. return (message, None)
  1111. jobtext = message[m.start():]
  1112. stripped_message = message[:m.start()].rstrip()
  1113. return (stripped_message, jobtext)
  1114. def prepareLogMessage(self, template, message, jobs):
  1115. """Edits the template returned from "p4 change -o" to insert
  1116. the message in the Description field, and the jobs text in
  1117. the Jobs field."""
  1118. result = ""
  1119. inDescriptionSection = False
  1120. for line in template.split("\n"):
  1121. if line.startswith("#"):
  1122. result += line + "\n"
  1123. continue
  1124. if inDescriptionSection:
  1125. if line.startswith("Files:") or line.startswith("Jobs:"):
  1126. inDescriptionSection = False
  1127. # insert Jobs section
  1128. if jobs:
  1129. result += jobs + "\n"
  1130. else:
  1131. continue
  1132. else:
  1133. if line.startswith("Description:"):
  1134. inDescriptionSection = True
  1135. line += "\n"
  1136. for messageLine in message.split("\n"):
  1137. line += "\t" + messageLine + "\n"
  1138. result += line + "\n"
  1139. return result
  1140. def patchRCSKeywords(self, file, pattern):
  1141. # Attempt to zap the RCS keywords in a p4 controlled file matching the given pattern
  1142. (handle, outFileName) = tempfile.mkstemp(dir='.')
  1143. try:
  1144. outFile = os.fdopen(handle, "w+")
  1145. inFile = open(file, "r")
  1146. regexp = re.compile(pattern, re.VERBOSE)
  1147. for line in inFile.readlines():
  1148. line = regexp.sub(r'$\1$', line)
  1149. outFile.write(line)
  1150. inFile.close()
  1151. outFile.close()
  1152. # Forcibly overwrite the original file
  1153. os.unlink(file)
  1154. shutil.move(outFileName, file)
  1155. except:
  1156. # cleanup our temporary file
  1157. os.unlink(outFileName)
  1158. print "Failed to strip RCS keywords in %s" % file
  1159. raise
  1160. print "Patched up RCS keywords in %s" % file
  1161. def p4UserForCommit(self,id):
  1162. # Return the tuple (perforce user,git email) for a given git commit id
  1163. self.getUserMapFromPerforceServer()
  1164. gitEmail = read_pipe(["git", "log", "--max-count=1",
  1165. "--format=%ae", id])
  1166. gitEmail = gitEmail.strip()
  1167. if not self.emails.has_key(gitEmail):
  1168. return (None,gitEmail)
  1169. else:
  1170. return (self.emails[gitEmail],gitEmail)
  1171. def checkValidP4Users(self,commits):
  1172. # check if any git authors cannot be mapped to p4 users
  1173. for id in commits:
  1174. (user,email) = self.p4UserForCommit(id)
  1175. if not user:
  1176. msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
  1177. if gitConfigBool("git-p4.allowMissingP4Users"):
  1178. print "%s" % msg
  1179. else:
  1180. die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
  1181. def lastP4Changelist(self):
  1182. # Get back the last changelist number submitted in this client spec. This
  1183. # then gets used to patch up the username in the change. If the same
  1184. # client spec is being used by multiple processes then this might go
  1185. # wrong.
  1186. results = p4CmdList("client -o") # find the current client
  1187. client = None
  1188. for r in results:
  1189. if r.has_key('Client'):
  1190. client = r['Client']
  1191. break
  1192. if not client:
  1193. die("could not get client spec")
  1194. results = p4CmdList(["changes", "-c", client, "-m", "1"])
  1195. for r in results:
  1196. if r.has_key('change'):
  1197. return r['change']
  1198. die("Could not get changelist number for last submit - cannot patch up user details")
  1199. def modifyChangelistUser(self, changelist, newUser):
  1200. # fixup the user field of a changelist after it has been submitted.
  1201. changes = p4CmdList("change -o %s" % changelist)
  1202. if len(changes) != 1:
  1203. die("Bad output from p4 change modifying %s to user %s" %
  1204. (changelist, newUser))
  1205. c = changes[0]
  1206. if c['User'] == newUser: return # nothing to do
  1207. c['User'] = newUser
  1208. input = marshal.dumps(c)
  1209. result = p4CmdList("change -f -i", stdin=input)
  1210. for r in

Large files files are truncated, but you can click here to view the full file