File indexing completed on 2024-03-24 17:21:43
0001 # -*- coding: UTF-8 -*- 0002 0003 """ 0004 Version control operations. 0005 0006 Collections of PO files are frequently kept under some sort of version control. 0007 This module provides typical version control operations, abstracted across 0008 various version control systems. 0009 0010 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net> 0011 @license: GPLv3 0012 """ 0013 0014 import os 0015 import re 0016 import shutil 0017 import tempfile 0018 0019 from pology import PologyError, _, n_ 0020 from pology.escape import escape_sh 0021 from pology.fsops import collect_system, system_wd, unicode_to_str, join_ncwd 0022 from pology.report import report, warning 0023 0024 0025 _vcskeys_by_pkey = {} 0026 _vcstypes_by_akey = {} 0027 0028 def _register_vcs (): 0029 0030 register = ( 0031 # First keyword is primary. 0032 (("none", "noop", "dummy"), 0033 VcsNoop), 0034 (("svn", "subversion"), 0035 VcsSubversion), 0036 (("git",), 0037 VcsGit), 0038 ) 0039 for vcskeys, vcstype in register: 0040 _vcskeys_by_pkey[vcskeys[0]] = vcskeys 0041 _vcstypes_by_akey.update([(x, vcstype) for x in vcskeys]) 0042 0043 0044 def available_vcs (flat=False): 0045 """ 0046 Get keywords of all available version control systems. 0047 0048 Some VCS have more than one keyword identifying them. 0049 If C{flat} is C{False}, a dictionary with primary keyword per VCS 0050 as keys, and tuple of all alternatives (including the main keyword) 0051 as values, is returned. 0052 If C{flat} is C{True}, all keywords are returned in a flat tuple. 0053 0054 @return: VCS keywords, as dictionary by primary or as a flat list of all 0055 @rtype: {(string, string)*} or [string*] 0056 """ 0057 0058 if flat: 0059 return list(_vcstypes_by_akey.keys()) 0060 else: 0061 return _vcskeys_by_pkey.copy() 0062 0063 0064 def make_vcs (vcskey): 0065 """ 0066 Factory for version control systems. 0067 0068 Desired VCS is identified by a keyword. Currently available: 0069 - dummy noop (C{none}, C{noop}, C{dummy}) 0070 - Subversion (C{svn}, C{subversion}) 0071 - Git (C{git}) 0072 0073 @param vcskey: keyword identifier of the VCS 0074 @type vcskey: string 0075 0076 @return: version control object 0077 @rtype: instance of L{VcsBase} 0078 """ 0079 0080 nkey = vcskey.strip().lower() 0081 vcstype = _vcstypes_by_akey.get(nkey) 0082 if not vcstype: 0083 raise PologyError( 0084 _("@info", 0085 "Unknown version control system requested by key '%(key)s'.", 0086 key=vcskey)) 0087 return vcstype() 0088 0089 0090 class VcsBase (object): 0091 """ 0092 Abstract base for VCS objects. 0093 """ 0094 0095 def add (self, paths, repadd=False): 0096 """ 0097 Add paths to version control. 0098 0099 It depends on the particular VCS what adding means, 0100 but in general it should be the point where the subsequent L{commit()} 0101 on the same path will record addition in the repository history. 0102 0103 Also a single path can be given instead of sequence of paths. 0104 0105 Actually added paths may be different from input paths, 0106 e.g. if an input path is already version controlled, 0107 or input path's parent directory was added as well. 0108 List of added paths can be requested with C{repadd} parameter, 0109 and it will become the second element of return value. 0110 0111 @param paths: paths to add 0112 @type paths: <string*> or string 0113 @param repadd: whether to report which paths were actually added 0114 @type repadd: bool 0115 0116 @return: C{True} if addition successful, possibly list of added paths 0117 @rtype: bool or (bool, [string*]) 0118 """ 0119 raise PologyError( 0120 _("@info", 0121 "Selected version control system does not define adding.")) 0122 0123 0124 def remove (self, path): 0125 """ 0126 Remove path from version control and from disk. 0127 0128 It depends on the particular VCS what removing means, 0129 but in general it should be the point where the subsequent L{commit()} 0130 on the same path will record removal in the repository history. 0131 0132 @param path: path to remove 0133 @type path: string 0134 0135 @return: C{True} if removal successful 0136 @rtype: bool 0137 """ 0138 0139 raise PologyError( 0140 _("@info", 0141 "Selected version control system does not define removing.")) 0142 0143 0144 def move (self, spath, dpath): 0145 """ 0146 Move versioned file or directory within the repository. 0147 0148 It depends on the particular VCS what moving means, 0149 but in general it should be the point where the subsequent L{commit()} 0150 on source and destination path (or their common parent directory) 0151 will record the move in the repository history. 0152 0153 @param spath: source path 0154 @type spath: string 0155 @param dpath: destination path 0156 @type dpath: string 0157 0158 @return: C{True} if moving successful 0159 @rtype: bool 0160 """ 0161 0162 raise PologyError( 0163 _("@info", 0164 "Selected version control system does not define moving.")) 0165 0166 0167 0168 def revision (self, path): 0169 """ 0170 Get current revision ID of the path. 0171 0172 @param path: path to query for revision 0173 @type path: string 0174 0175 @return: revision ID 0176 @rtype: string 0177 """ 0178 0179 raise PologyError( 0180 _("@info", 0181 "Selected version control system does not define " 0182 "revision query.")) 0183 0184 0185 def is_clear (self, path): 0186 """ 0187 Check if the path is in clear state. 0188 0189 Clear state means none of: not version-controlled, modified, added... 0190 0191 @param path: path to check the state of 0192 @type path: string 0193 0194 @return: C{True} if clear 0195 @rtype: bool 0196 """ 0197 0198 raise PologyError( 0199 _("@info", 0200 "Selected version control system does not define state query.")) 0201 0202 0203 def is_versioned (self, path): 0204 """ 0205 Check if path is under version control. 0206 0207 @param path: path to check 0208 @type path: string 0209 0210 @return: C{True} if versioned 0211 @rtype: bool 0212 """ 0213 0214 raise PologyError( 0215 _("@info", 0216 "Selected version control system does not define " 0217 "checking whether a path is version controlled.")) 0218 0219 0220 def export (self, path, rev, dstpath, rewrite=None): 0221 """ 0222 Export a versioned file or directory. 0223 0224 Makes a copy of versioned file or directory pointed to by 0225 local path C{path}, in the revision C{rev}, to destination C{dstpath}. 0226 If C{rev} is C{None}, the clean version of C{path} according 0227 to current local repository state is copied to C{dstpath}. 0228 0229 Final repository path, as determined from C{path}, can be filtered 0230 through an external function C{rewrite} before being used. 0231 The function takes as arguments the path and revision strings. 0232 This can be useful, for example, to reroute remote repository URL. 0233 0234 @param path: path of the versioned file or directory in local repository 0235 @type path: string 0236 @param rev: revision to export 0237 @type rev: string or C{None} 0238 @param dstpath: file path to export to 0239 @type dstpath: string 0240 @param rewrite: function to filter resolved repository path 0241 @type rewrite: (string, string)->string or None 0242 0243 @return: C{True} if fetching succeeded, C{False} otherwise 0244 @rtype: bool 0245 """ 0246 0247 raise PologyError( 0248 _("@info", 0249 "Selected version control system does not define " 0250 "fetching of a versioned path.")) 0251 0252 0253 def commit (self, paths, message=None, msgfile=None, incparents=True): 0254 """ 0255 Commit paths to the repository. 0256 0257 Paths can include any number of files and directories. 0258 Also a single path string can be given instead of a sequence. 0259 It depends on the particular VCS what committing means, 0260 but in general it should be the earliest level at which 0261 modifications are recorded in the repository history. 0262 0263 Commit message can be given either directly, through C{message} 0264 parameter, or read from a file with path given by C{msgfile}. 0265 If both C{message} and C{msgfile} are given, 0266 C{message} takes precedence and C{msgfile} is ignored. 0267 If the commit message is not given, VCS should ask for one as usual 0268 (pop an editor window, or whatever the user has configured). 0269 0270 Some VCS require that the parent directory of a path to be committed 0271 has been committed itself or included in the commit list if not. 0272 If that is the case, C{incparents} parameter determines if 0273 this function should assure that non-committed parents are included 0274 into the commit list too. This may be expensive to check, 0275 so it is good to disable it if all parents are known to be 0276 committed or included in the input paths. 0277 0278 @param paths: paths to commit 0279 @type paths: <string*> or string 0280 @param message: commit message 0281 @type message: string 0282 @param msgfile: path to file with the commit message 0283 @type msgfile: string 0284 @param incparents: whether to automatically include non-committed 0285 parents in the commit list 0286 @type incparents: bool 0287 0288 @return: C{True} if committing succeeded, C{False} otherwise 0289 @rtype: bool 0290 """ 0291 0292 raise PologyError( 0293 _("@info", 0294 "Selected version control system does not define " 0295 "committing of paths.")) 0296 0297 0298 def log (self, path, rev1=None, rev2=None): 0299 """ 0300 Get revision log of the path. 0301 0302 Revision log entry consists of revision ID, commiter name, 0303 date string, and commit message. 0304 Except the revision ID, any of these may be empty strings, 0305 depending on the particular VCS. 0306 The log is ordered from earliest to newest revision. 0307 0308 A section of entries between revisions C{rev1} (inclusive) 0309 and C{rev2} (exclusive) can be returned instead of the whole log. 0310 If C{rev1} is C{None}, selected IDs start from the first in the log. 0311 If C{rev2} is C{None}, selected IDs end with the last in the log. 0312 0313 If either C{rev1} or C{rev2} is not C{None} and does not exist in 0314 the path's log, or the path is not versioned, empty log is returned. 0315 0316 @param path: path to query for revisions 0317 @type path: string 0318 @param rev1: entries starting from this revision (inclusive) 0319 @type rev1: string 0320 @param rev2: entries up to this revision (exclusive) 0321 @type rev2: string 0322 0323 @return: revision ID, committer name, date string, commit message 0324 @rtype: [(string*4)*] 0325 """ 0326 0327 raise PologyError( 0328 _("@info", 0329 "Selected version control system does not define " 0330 "revision history query.")) 0331 0332 0333 def to_commit (self, path): 0334 """ 0335 Get paths which need to be committed within the given path. 0336 0337 Input path can be either a file or directory. 0338 If it is a directory, it depends on VCS whether it will 0339 only report files within it that need to be committed, 0340 or subdirectories too (including the given directory). 0341 0342 @param path: path to query for non-committed paths 0343 @type path: string 0344 0345 @return: non-committed paths 0346 @rtype: [string*] 0347 """ 0348 0349 raise PologyError( 0350 _("@info", 0351 "Selected version control system does not define " 0352 "listing of non-committed paths.")) 0353 0354 0355 def diff (self, path, rev1=None, rev2=None): 0356 """ 0357 Get diff between revisions of the given path. 0358 0359 Unified diff is computed and reported as list of 2-tuples, 0360 where the first element is a tag, and the second the payload. 0361 For tags C{" "}, C{"+"}, and C{"-"}, the payload is the line 0362 (without newline) which was equal, added or removed, respectively. 0363 Payload for tag C{":"} is the path of the diffed file, 0364 and for C{"@"} the 4-tuple of old start line, old number of lines, 0365 new start line, and new number of lines, which are represented 0366 by the following difference segment. 0367 0368 Diffs can be requested between specific revisions. 0369 If both C{rev1} and C{rev2} are C{None}, 0370 diff is taken from last known commit to working copy. 0371 If only C{rev2} is C{None} diff is taken from C{rev1} to working copy. 0372 0373 @param path: path to query for modified lines 0374 @type path: string 0375 @param rev1: diff from this revision 0376 @type rev1: string 0377 @param rev2: diff to this revision 0378 @type rev2: string 0379 0380 @return: tagged unified diff 0381 @rtype: [(string, string or (int, int, int, int))*] 0382 """ 0383 0384 raise PologyError( 0385 _("@info", 0386 "Selected version control system does not define diffing.")) 0387 0388 0389 def revert (self, path): 0390 """ 0391 Revert a versioned file or directory. 0392 0393 The path is reverted to the clean version of itself according 0394 to current local repository state. 0395 0396 @param path: path of the versioned file or directory in local repository 0397 @type path: string 0398 0399 @return: C{True} if reverting succeeded, C{False} otherwise 0400 @rtype: bool 0401 """ 0402 0403 raise PologyError( 0404 _("@info", 0405 "Selected version control system does not define " 0406 "reverting a versioned path.")) 0407 0408 0409 class VcsNoop (VcsBase): 0410 """ 0411 VCS: Dummy VCS which perform only file system operations. 0412 """ 0413 0414 def add (self, paths, repadd=False): 0415 # Base override. 0416 0417 return True if not repadd else [True, paths] 0418 0419 0420 def remove (self, path): 0421 # Base override. 0422 0423 if os.path.isdir(path): 0424 shutil.rmtree(path) 0425 else: 0426 os.remove(path) 0427 return True 0428 0429 0430 def move (self, spath, dpath): 0431 # Base override. 0432 0433 shutil.move(spath, dpath) 0434 return True 0435 0436 0437 def revision (self, path): 0438 # Base override. 0439 0440 return "" 0441 0442 0443 def is_clear (self, path): 0444 # Base override. 0445 0446 return True 0447 0448 0449 def is_versioned (self, path): 0450 # Base override. 0451 0452 return True 0453 0454 0455 def export (self, path, rev, dstpath, rewrite=None): 0456 # Base override. 0457 0458 if rev is not None: 0459 return False 0460 0461 try: 0462 os.shutil.copyfile(path, dstpath) 0463 except: 0464 return False 0465 return True 0466 0467 0468 def commit (self, paths, message=None, msgfile=None, incparents=True): 0469 # Base override. 0470 0471 return True 0472 0473 0474 def log (self, path, rev1=None, rev2=None): 0475 # Base override. 0476 0477 return [] 0478 0479 0480 def to_commit (self, path): 0481 # Base override. 0482 0483 return [] 0484 0485 0486 def revert (self, path): 0487 # Base override. 0488 0489 return True 0490 0491 0492 class VcsSubversion (VcsBase): 0493 """ 0494 VCS: Subversion. 0495 """ 0496 0497 def __init__ (self): 0498 0499 # Environment to cancel any localization in output of operations, 0500 # for methods which need to parse the output. 0501 self._env = os.environ.copy() 0502 self._env["LC_ALL"] = "C" 0503 0504 0505 def add (self, paths, repadd=False): 0506 # Base override. 0507 0508 if isinstance(paths, str): 0509 paths = [paths] 0510 if not paths: 0511 return True 0512 0513 tmppath = _temp_paths_file(paths) 0514 res = collect_system(["svn", "add", "--force", "--parents", 0515 "--targets", tmppath], 0516 env=self._env) 0517 success = (res[2] == 0) 0518 os.remove(tmppath) 0519 0520 if repadd: 0521 apaths = [] 0522 for line in res[0].split("\n"): 0523 if line.startswith("A"): 0524 apaths.append(line[1:].strip()) 0525 return success, apaths 0526 else: 0527 return success 0528 0529 0530 def remove (self, path): 0531 # Base override. 0532 0533 if collect_system(["svn", "remove", path])[2] != 0: 0534 return False 0535 0536 return True 0537 0538 0539 def move (self, spath, dpath): 0540 # Base override. 0541 0542 if collect_system(["svn", "move", "--parents", 0543 self._ep(spath), dpath])[2] != 0: 0544 return False 0545 return True 0546 0547 0548 def revision (self, path): 0549 # Base override. 0550 0551 res = collect_system(["svn", "info", self._ep(path)], env=self._env) 0552 rx = re.compile(r"^Last Changed Rev: *([0-9]+)", re.I) 0553 revid = "" 0554 for line in res[0].split("\n"): 0555 m = rx.search(line) 0556 if m: 0557 revid = m.group(1) 0558 break 0559 0560 return revid 0561 0562 0563 def is_clear (self, path): 0564 # Base override. 0565 0566 res = collect_system(["svn", "status", path], env=self._env) 0567 clear = not re.search(r"^\S", res[0]) 0568 0569 return clear 0570 0571 0572 def is_versioned (self, path): 0573 # Base override. 0574 0575 res = collect_system(["svn", "info", self._ep(path)], env=self._env) 0576 if res[-1] != 0: 0577 return False 0578 0579 rx = re.compile(r"^Repository", re.I) 0580 for line in res[0].split("\n"): 0581 if rx.search(line): 0582 return True 0583 0584 return False 0585 0586 0587 def export (self, path, rev, dstpath, rewrite=None): 0588 # Base override. 0589 0590 if rev is None: 0591 res = collect_system(["svn", "export", "--force", self._ep(path), 0592 "-r", "BASE", dstpath]) 0593 if res[-1] != 0: 0594 return False 0595 return True 0596 0597 res = collect_system(["svn", "info", self._ep(path)], env=self._env) 0598 if res[-1] != 0: 0599 return False 0600 rx = re.compile(r"^URL:\s*(\S+)", re.I) 0601 rempath = None 0602 for line in res[0].split("\n"): 0603 m = rx.search(line) 0604 if m: 0605 rempath = m.group(1) 0606 break 0607 if not rempath: 0608 return False 0609 0610 if rewrite: 0611 rempath = rewrite(rempath, rev) 0612 0613 if collect_system(["svn", "export", "--force", self._ep(rempath), 0614 "-r", rev, dstpath])[-1] != 0: 0615 return False 0616 0617 return True 0618 0619 0620 def commit (self, paths, message=None, msgfile=None, incparents=True): 0621 # Base override. 0622 0623 if isinstance(paths, str): 0624 paths = [paths] 0625 if not paths: 0626 return True 0627 0628 if incparents: 0629 # Move up any path that needs its parent committed too. 0630 paths_mod = [] 0631 for path in paths: 0632 path_mod = path 0633 while True: 0634 path_mod_up = os.path.dirname(path_mod) 0635 if self.revision(path_mod_up): 0636 break 0637 elif not path_mod_up or not self.is_versioned(path_mod_up): 0638 # Let simply Subversion complain. 0639 path_mod = path 0640 break 0641 else: 0642 path_mod = path_mod_up 0643 paths_mod.append(path_mod) 0644 paths = paths_mod 0645 0646 cmdline = ["svn", "commit"] 0647 if message is not None: 0648 cmdline += ["-m", message] 0649 elif msgfile is not None: 0650 cmdline += ["-F", msgfile] 0651 tmppath = _temp_paths_file(paths) 0652 cmdline += ["--targets", tmppath] 0653 # Do not use collect_system(), user may need to input stuff. 0654 cmdstr = " ".join(map(escape_sh, cmdline)) 0655 success = (os.system(unicode_to_str(cmdstr)) == 0) 0656 os.remove(tmppath) 0657 0658 return success 0659 0660 0661 def log (self, path, rev1=None, rev2=None): 0662 # Base override. 0663 0664 res = collect_system(["svn", "log", self._ep(path)], env=self._env) 0665 if res[-1] != 0: 0666 return [] 0667 rev = "" 0668 next_rev, next_cmsg = list(range(2)) 0669 entries = [] 0670 next = -1 0671 for line in res[0].strip().split("\n"): 0672 if line.startswith("----------"): 0673 if rev: 0674 cmsg = "\n".join(cmsg).strip("\n") 0675 entries.append((rev, user, dstr, cmsg)) 0676 cmsg = [] 0677 next = next_rev 0678 elif next == next_rev: 0679 lst = line.split("|") 0680 rev, user, dstr = [x.strip() for x in lst[:3]] 0681 rev = rev[1:] # strip initial "r" 0682 next = next_cmsg 0683 elif next == next_cmsg: 0684 cmsg += [line] 0685 0686 entries.reverse() 0687 0688 return _crop_log(entries, rev1, rev2) 0689 0690 0691 def to_commit (self, path): 0692 # Base override. 0693 0694 res = collect_system(["svn", "status", path], env=self._env) 0695 if res[-1] != 0: 0696 return [] 0697 0698 ncpaths = [] 0699 for line in res[0].split("\n"): 0700 if line[:1] in ("A", "M"): 0701 path = line[1:].strip() 0702 ncpaths.append(path) 0703 0704 return ncpaths 0705 0706 0707 def diff (self, path, rev1=None, rev2=None): 0708 # Base override. 0709 0710 if rev1 is not None and rev2 is not None: 0711 rspec = "-r %s:%s" % (rev1, rev2) 0712 elif rev1 is not None: 0713 rspec = "-r %s" % rev1 0714 elif rev2 is not None: 0715 raise PologyError( 0716 _("@info \"Subversion\" is a version control system", 0717 "Subversion cannot diff from working copy " 0718 "to a named revision.")) 0719 else: 0720 rspec = "" 0721 0722 res = collect_system(["svn", "diff", path, rspec], env=self._env) 0723 if res[-1] != 0: 0724 warning(_("@info", 0725 "Subversion reports it cannot diff path '%(path)s':\n" 0726 "%(msg)s", 0727 path=path, msg=res[1])) 0728 return [] 0729 0730 udiff = [] 0731 nskip = 0 0732 for line in res[0].split("\n"): 0733 if nskip > 0: 0734 nskip -= 1 0735 continue 0736 0737 if line.startswith("Index:"): 0738 udiff.append((":", line[line.find(":") + 1:].strip())) 0739 nskip = 3 0740 elif line.startswith("@@"): 0741 m = re.search(r"-(\d+),(\d+) *\+(\d+),(\d+)", line) 0742 spans = tuple(map(int, m.groups())) if m else (0, 0, 0, 0) 0743 udiff.append(("@", spans)) 0744 elif line.startswith(" "): 0745 udiff.append((" ", line[1:])) 0746 elif line.startswith("-"): 0747 udiff.append(("-", line[1:])) 0748 elif line.startswith("+"): 0749 udiff.append(("+", line[1:])) 0750 0751 return udiff 0752 0753 0754 def revert (self, path): 0755 # Base override. 0756 0757 res = collect_system(["svn", "revert", "-R", path], env=self._env) 0758 if res[-1] != 0: 0759 warning(_("@info", 0760 "Subversion reports it cannot revert path '%(path)s':\n" 0761 "%(msg)s", 0762 path=path, msg=res[1])) 0763 return False 0764 0765 return True 0766 0767 0768 def _ep (self, path): 0769 #if "@" in os.path.basename(os.path.normpath(path)): 0770 if "@" in path: 0771 path = "%s@" % path 0772 return path 0773 0774 0775 class VcsGit (VcsBase): 0776 """ 0777 VCS: Git. 0778 """ 0779 0780 def __init__ (self): 0781 0782 # Environment to cancel any localization in output of operations, 0783 # for methods which need to parse the output. 0784 self._env = os.environ.copy() 0785 self._env["LC_ALL"] = "C" 0786 0787 0788 def _gitroot (self, paths): 0789 0790 single = False 0791 if isinstance(paths, str): 0792 paths = [paths] 0793 single = True 0794 0795 # Take first path as referent. 0796 path = os.path.abspath(paths[0]) 0797 0798 root = None 0799 if os.path.isfile(path): 0800 pdir = os.path.dirname(path) 0801 else: 0802 pdir = path 0803 while True: 0804 gitpath = os.path.join(pdir, ".git") 0805 if os.path.isdir(gitpath): 0806 root = pdir 0807 break 0808 pdir_p = pdir 0809 pdir = os.path.dirname(pdir) 0810 if pdir == pdir_p: 0811 break 0812 0813 if root is None: 0814 raise PologyError( 0815 _("@info \"Git\" is a version control system", 0816 "Cannot find Git repository for '%(path)s'.", 0817 path=path)) 0818 0819 rpaths = [] 0820 for path in paths: 0821 path = os.path.abspath(path) 0822 path = path[len(root) + len(os.path.sep):] 0823 rpaths.append(path) 0824 0825 if single: 0826 return root, rpaths[0] 0827 else: 0828 return root, rpaths 0829 0830 0831 def add (self, paths, repadd=False): 0832 # Base override. 0833 0834 if isinstance(paths, str): 0835 paths = [paths] 0836 if not paths: 0837 return True 0838 0839 root, paths = self._gitroot(paths) 0840 0841 success = True 0842 apaths = [] 0843 for path in paths: 0844 if collect_system(["git", "add", path], wdir=root)[2] != 0: 0845 success = False 0846 break 0847 apaths.append(path) 0848 0849 return success if not repadd else [success, apaths] 0850 0851 0852 def remove (self, path): 0853 # Base override. 0854 0855 if os.path.isdir(path): 0856 warning(_("@info", 0857 "Git cannot remove directories (tried on '%(path)s').", 0858 path=path)) 0859 return False 0860 0861 root, path = self._gitroot(path) 0862 0863 if collect_system(["git", "rm", path], wdir=root)[2] != 0: 0864 return False 0865 0866 return True 0867 0868 0869 def move (self, spath, dpath): 0870 # Base override. 0871 0872 root1, spath = self._gitroot(spath) 0873 root2, dpath = self._gitroot(dpath) 0874 if root1 != root2: 0875 warning(_("@info", 0876 "Trying to move paths between different repositories.")) 0877 return False 0878 0879 if collect_system(["git", "mv", spath, dpath], wdir=root1)[2] != 0: 0880 return False 0881 0882 return True 0883 0884 0885 def revision (self, path): 0886 # Base override. 0887 0888 root, path = self._gitroot(path) 0889 0890 res = collect_system(["git", "log", path], wdir=root, env=self._env) 0891 rx = re.compile(r"^commit\s*([0-9abcdef]+)", re.I) 0892 revid = "" 0893 for line in res[0].split("\n"): 0894 m = rx.search(line) 0895 if m: 0896 revid = m.group(1) 0897 break 0898 0899 return revid 0900 0901 0902 def is_clear (self, path): 0903 # Base override. 0904 0905 root, path = self._gitroot(path) 0906 0907 res = collect_system(["git", "status", path], wdir=root, env=self._env) 0908 rx = re.compile(r"\bmodified:\s*(\S.*)", re.I) 0909 for line in res[0].split("\n"): 0910 m = rx.search(line) 0911 if m: 0912 mpath = m.group(1) 0913 if os.path.isfile(path): 0914 if mpath == path: 0915 return False 0916 else: 0917 if not path or mpath[len(path):].startswith(os.path.sep): 0918 return False 0919 0920 return True 0921 0922 0923 def is_versioned (self, path): 0924 # Base override. 0925 0926 root, path = self._gitroot(path) 0927 if not path: 0928 return True 0929 0930 res = collect_system(["git", "status"], wdir=root, env=self._env) 0931 rx = re.compile(r"untracked.*?:", re.I) 0932 m = rx.search(res[0]) 0933 if m: 0934 for line in res[0][m.end():].split("\n"): 0935 line = line.lstrip("#").strip() 0936 if line == path: 0937 return False 0938 0939 return True 0940 0941 0942 def export (self, path, rev, dstpath, rewrite=None): 0943 # Base override. 0944 0945 root, path = self._gitroot(path) 0946 ret = True 0947 0948 if rev is None: 0949 rev = "HEAD" 0950 0951 if rewrite: 0952 path = rewrite(path, rev) 0953 0954 # FIXME: Better temporary location." 0955 # FIXME: Use list command lines (so must replace piping too). 0956 tarpdir = "/tmp" 0957 tarbdir = "git-archive-tree%d" % os.getpid() 0958 res = collect_system(" git archive --prefix=%s/ %s %s " 0959 "| (cd %s && tar xf -)" 0960 % (tarbdir, rev, path, tarpdir), 0961 wdir=root) 0962 if res[2] == 0: 0963 tardir = os.path.join(tarpdir, tarbdir) 0964 tarpath = os.path.join(tardir, path) 0965 try: 0966 shutil.move(tarpath, dstpath) 0967 except: 0968 ret = False 0969 if os.path.isdir(tardir): 0970 shutil.rmtree(tardir) 0971 else: 0972 ret = False 0973 0974 return ret 0975 0976 0977 def commit (self, paths, message=None, msgfile=None, incparents=True): 0978 # Base override. 0979 0980 if isinstance(paths, str): 0981 paths = [paths] 0982 if not paths: 0983 return True 0984 0985 opaths = paths 0986 root, paths = self._gitroot(paths) 0987 0988 # Check if all paths are versioned. 0989 # Add to index any modified paths that have not been added. 0990 for opath in opaths: 0991 if not self.is_versioned(opath): 0992 warning(_("@info" 0993 "Git cannot commit non-versioned path '%(path)s'.", 0994 path=opath)) 0995 return False 0996 if os.path.exists(opath) and not self.add(opath): 0997 warning(_("@info" 0998 "Git cannot add path '%(path)s' to index.", 0999 path=opath)) 1000 return False 1001 1002 # Reset all paths in index which have not been given to commit. 1003 ipaths = self._paths_to_commit(root) 1004 rpaths = list(set(ipaths).difference(paths)) 1005 if rpaths: 1006 warning(_("@info", 1007 "Git is resetting paths in index which are " 1008 "not to be committed.")) 1009 cmdline = "git reset %s" % " ".join(rpaths) 1010 system_wd(unicode_to_str(cmdline), root) 1011 # ...seems to return != 0 even if it did what it was told to. 1012 1013 # Commit the index. 1014 cmdline = ["git", "commit"] 1015 if message is not None: 1016 cmdline += ["-m", message] 1017 elif msgfile is not None: 1018 cmdline += ["-F", msgfile] 1019 # Do not use collect_system(), user may need to input stuff. 1020 cmdstr = " ".join(map(escape_sh, cmdline)) 1021 if system_wd(unicode_to_str(cmdstr), root) != 0: 1022 return False 1023 1024 return True 1025 1026 1027 def log (self, path, rev1=None, rev2=None): 1028 # Base override. 1029 1030 root, path = self._gitroot(path) 1031 1032 res = collect_system(["git", "log", path], wdir=root, env=self._env) 1033 if res[-1] != 0: 1034 return [] 1035 rev = "" 1036 next_auth, next_date, next_cmsg = list(range(3)) 1037 next = -1 1038 entries = [] 1039 lines = res[0].split("\n") 1040 for i in range(len(lines) + 1): 1041 if i < len(lines): 1042 line = lines[i] 1043 if i == len(lines) or line.startswith("commit"): 1044 if rev: 1045 cmsg = "\n".join(cmsg).strip("\n") 1046 entries.append((rev, user, dstr, cmsg)) 1047 rev = line[line.find(" ") + 1:].strip() 1048 cmsg = [] 1049 next = next_auth 1050 elif next == next_auth: 1051 user = line[line.find(":") + 1:].strip() 1052 next = next_date 1053 elif next == next_date: 1054 dstr = line[line.find(":") + 1:].strip() 1055 next = next_cmsg 1056 elif next == next_cmsg: 1057 cmsg += [line[4:]] 1058 1059 entries.reverse() 1060 1061 return _crop_log(entries, rev1, rev2) 1062 1063 1064 def to_commit (self, path): 1065 # Base override. 1066 1067 root, path = self._gitroot(path) 1068 1069 ncpaths = self._paths_to_commit(root, path or ".") 1070 1071 ncpaths = [join_ncwd(root, p) for p in ncpaths] 1072 1073 return ncpaths 1074 1075 1076 def _paths_to_commit (self, root, path=None): 1077 1078 if path: 1079 cmdline = ["git", "status", path] 1080 else: 1081 cmdline = ["git", "status"] 1082 res = collect_system(cmdline, wdir=root, env=self._env) 1083 1084 sect_rx = re.compile(r"^(?:# )?(\S.*):$", re.I) 1085 file_rx = re.compile(r"^#?\s+.*\w:\s*(.+?)\s*$", re.I) 1086 inlist = False 1087 ipaths = [] 1088 for line in res[0].split("\n"): 1089 m = sect_rx.search(line) 1090 if m: 1091 mstr = m.group(1) 1092 if ( mstr.endswith("to be committed") # git 1.6.x 1093 or mstr.endswith("but not updated") # git 1.7.x 1094 or mstr.endswith("not staged for commit") # git 1.7.x 1095 ): 1096 inlist = True 1097 else: 1098 break 1099 if not inlist: 1100 continue 1101 m = file_rx.search(line) 1102 if m: 1103 ipaths.append(m.group(1)) 1104 1105 return ipaths 1106 1107 1108 def diff (self, path, rev1=None, rev2=None): 1109 # Base override. 1110 1111 root, path = self._gitroot(path) 1112 1113 if rev1 is not None and rev2 is not None: 1114 rspec = "%s..%s" % (rev1, rev2) 1115 elif rev1 is not None: 1116 rspec = "%s" % rev1 1117 elif rev2 is not None: 1118 raise PologyError( 1119 _("@info" 1120 "Git cannot diff from non-staged paths to a commit.")) 1121 else: 1122 rspec = "" 1123 1124 res = collect_system(["git", "diff", rspec, path], 1125 wdir=root, env=self._env) 1126 if res[-1] != 0: 1127 warning(_("@info" 1128 "Git reports it cannot diff path '%(path)s':\n" 1129 "%(msg)s", 1130 path=path, msg=res[1])) 1131 return [] 1132 1133 udiff = [] 1134 nskip = 0 1135 for line in res[0].split("\n"): 1136 if nskip > 0: 1137 nskip -= 1 1138 continue 1139 1140 if line.startswith("diff"): 1141 m = re.search(r"a/(.*?) *b/", line) 1142 udiff.append((":", m.group(1) if m else "")) 1143 nskip = 3 1144 elif line.startswith("@@"): 1145 m = re.search(r"-(\d+),(\d+) *\+(\d+),(\d+)", line) 1146 spans = tuple(map(int, m.groups())) if m else (0, 0, 0, 0) 1147 udiff.append(("@", spans)) 1148 elif line.startswith(" "): 1149 udiff.append((" ", line[1:])) 1150 elif line.startswith("-"): 1151 udiff.append(("-", line[1:])) 1152 elif line.startswith("+"): 1153 udiff.append(("+", line[1:])) 1154 1155 return udiff 1156 1157 1158 def revert (self, path): 1159 # Base override. 1160 1161 res = collect_system(["git", "checkout", path], 1162 wdir=root, env=self._env) 1163 if res[-1] != 0: 1164 warning(_("@info" 1165 "Git reports it cannot revert path '%(path)s':\n" 1166 "%(msg)s", 1167 path=path, msg=res[1])) 1168 return [] 1169 1170 return True 1171 1172 1173 def _crop_log (entries, rev1, rev2): 1174 1175 start = 0 1176 if rev1 is not None: 1177 while start < len(entries): 1178 if entries[start][0] == rev1: 1179 break 1180 start += 1 1181 1182 end = len(entries) 1183 if rev2 is not None: 1184 while end > 0: 1185 end -= 1 1186 if entries[end][0] == rev2: 1187 break 1188 1189 return entries[start:end] 1190 1191 1192 def _temp_paths_file (paths): 1193 1194 content = unicode_to_str("\n".join(paths) + "\n") 1195 tmpf, tmppath = tempfile.mkstemp() 1196 os.write(tmpf, content) 1197 os.close(tmpf) 1198 return tmppath 1199 1200 1201 _register_vcs()