repo.py revision 13540:da30e62884ee
12221SN/A#!/usr/bin/env python2.7
22221SN/A#
32221SN/A# Copyright (c) 2016 ARM Limited
42221SN/A# All rights reserved
52221SN/A#
62221SN/A# The license below extends only to copyright in the software and shall
72221SN/A# not be construed as granting a license to any other intellectual
82221SN/A# property including but not limited to intellectual property relating
92221SN/A# to a hardware implementation of the functionality of the software
102221SN/A# licensed hereunder.  You may use the software subject to the license
112221SN/A# terms below provided that you ensure that this notice is replicated
122221SN/A# unmodified and in its entirety in all distributions of the software,
132221SN/A# modified or unmodified, in source code or in binary form.
142221SN/A#
152221SN/A# Redistribution and use in source and binary forms, with or without
162221SN/A# modification, are permitted provided that the following conditions are
172221SN/A# met: redistributions of source code must retain the above copyright
182221SN/A# notice, this list of conditions and the following disclaimer;
192221SN/A# redistributions in binary form must reproduce the above copyright
202221SN/A# notice, this list of conditions and the following disclaimer in the
212221SN/A# documentation and/or other materials provided with the distribution;
222221SN/A# neither the name of the copyright holders nor the names of its
232221SN/A# contributors may be used to endorse or promote products derived from
242221SN/A# this software without specific prior written permission.
252221SN/A#
262221SN/A# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
272665Ssaidi@eecs.umich.edu# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
282665Ssaidi@eecs.umich.edu# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
292665Ssaidi@eecs.umich.edu# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
302221SN/A# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
312221SN/A# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
323890Ssaidi@eecs.umich.edu# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
333890Ssaidi@eecs.umich.edu# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
342221SN/A# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
354997Sgblack@eecs.umich.edu# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
367678Sgblack@eecs.umich.edu# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
372221SN/A#
382221SN/A# Authors: Andreas Sandberg
392221SN/A
402221SN/Afrom abc import *
412223SN/Aimport os
422221SN/Aimport subprocess
432221SN/A
443415Sgblack@eecs.umich.edufrom region import *
453415Sgblack@eecs.umich.edufrom style import modified_regions
462221SN/A
474997Sgblack@eecs.umich.educlass AbstractRepo(object):
484997Sgblack@eecs.umich.edu    __metaclass__ = ABCMeta
493573Sgblack@eecs.umich.edu
502221SN/A    def file_path(self, fname):
512221SN/A        """Get the absolute path to a file relative within the repository. The
523576Sgblack@eecs.umich.edu        input file name must be a valid path within the repository.
533576Sgblack@eecs.umich.edu
543576Sgblack@eecs.umich.edu        """
553576Sgblack@eecs.umich.edu        return os.path.join(self.repo_base(), fname)
563576Sgblack@eecs.umich.edu
573576Sgblack@eecs.umich.edu    def in_repo(self, fname):
583576Sgblack@eecs.umich.edu        """Check if a path points to something within the repository base. Not
593576Sgblack@eecs.umich.edu        that this does not check for the presence of the object in the
603576Sgblack@eecs.umich.edu        file system as it could exist in the index without being in
613573Sgblack@eecs.umich.edu        the file system.
623573Sgblack@eecs.umich.edu
633573Sgblack@eecs.umich.edu        """
643573Sgblack@eecs.umich.edu        fname = os.path.abspath(fname)
653573Sgblack@eecs.umich.edu        repo_path = os.path.abspath(self.repo_base())
663576Sgblack@eecs.umich.edu
673573Sgblack@eecs.umich.edu        return os.path.commonprefix([repo_path, fname]) == repo_path
683573Sgblack@eecs.umich.edu
692221SN/A    def repo_path(self, fname):
707678Sgblack@eecs.umich.edu        """Get the path of a file relative to the repository base. The input
717678Sgblack@eecs.umich.edu        file name is assumed to be an absolute path or a path relative
722221SN/A        to the current working directory.
732223SN/A
742223SN/A        """
752223SN/A        return os.path.relpath(fname, self.repo_base())
763576Sgblack@eecs.umich.edu
772221SN/A    def get_file(self, name):
782221SN/A        """Get the contents of a file in the file system using a path relative
793573Sgblack@eecs.umich.edu        to the repository root.
803573Sgblack@eecs.umich.edu
812221SN/A        """
823573Sgblack@eecs.umich.edu        with open(self.file_path(name), "r") as f:
833573Sgblack@eecs.umich.edu            return f.read()
842221SN/A
857741Sgblack@eecs.umich.edu    @abstractmethod
867741Sgblack@eecs.umich.edu    def repo_base(self):
877741Sgblack@eecs.umich.edu        """Get the path to the base of the repository"""
887741Sgblack@eecs.umich.edu        pass
897741Sgblack@eecs.umich.edu
907741Sgblack@eecs.umich.edu    @abstractmethod
917741Sgblack@eecs.umich.edu    def staged_files(self):
923576Sgblack@eecs.umich.edu        """Get a tuple describing the files that have been staged for a
933576Sgblack@eecs.umich.edu        commit: (list of new, list of modified)
943576Sgblack@eecs.umich.edu
953573Sgblack@eecs.umich.edu        """
963573Sgblack@eecs.umich.edu        pass
973576Sgblack@eecs.umich.edu
983576Sgblack@eecs.umich.edu    @abstractmethod
997678Sgblack@eecs.umich.edu    def staged_regions(self, fname, context=0):
1007678Sgblack@eecs.umich.edu        """Get modified regions that will be committed by the next commit
1017678Sgblack@eecs.umich.edu        command
1027678Sgblack@eecs.umich.edu
1033576Sgblack@eecs.umich.edu        """
1043576Sgblack@eecs.umich.edu        pass
1053576Sgblack@eecs.umich.edu
1063576Sgblack@eecs.umich.edu    @abstractmethod
1073576Sgblack@eecs.umich.edu    def modified_regions(self, fname, context=0):
1083576Sgblack@eecs.umich.edu        """Get modified regions that have been staged for commit or are
1093576Sgblack@eecs.umich.edu        present in the file system.
1103576Sgblack@eecs.umich.edu
1113576Sgblack@eecs.umich.edu        """
1123576Sgblack@eecs.umich.edu        pass
1133576Sgblack@eecs.umich.edu
1143576Sgblack@eecs.umich.educlass GitRepo(AbstractRepo):
1153576Sgblack@eecs.umich.edu    def __init__(self):
1163576Sgblack@eecs.umich.edu        self.git = "git"
1177741Sgblack@eecs.umich.edu        self._head_revision = None
1183576Sgblack@eecs.umich.edu        self._repo_base = None
1193576Sgblack@eecs.umich.edu
1203576Sgblack@eecs.umich.edu    def repo_base(self):
1213576Sgblack@eecs.umich.edu        if self._repo_base is None:
1223576Sgblack@eecs.umich.edu            self._repo_base = subprocess.check_output(
1233576Sgblack@eecs.umich.edu                [ self.git, "rev-parse", "--show-toplevel" ]).rstrip("\n")
1243576Sgblack@eecs.umich.edu
1257741Sgblack@eecs.umich.edu        return self._repo_base
1263576Sgblack@eecs.umich.edu
1277741Sgblack@eecs.umich.edu    def staged_files(self):
1283576Sgblack@eecs.umich.edu        added = []
1293576Sgblack@eecs.umich.edu        modified = []
1303576Sgblack@eecs.umich.edu        for action, fname in self.status(filter="MA", cached=True):
1313576Sgblack@eecs.umich.edu            if action == "M":
1323576Sgblack@eecs.umich.edu                modified.append(fname)
1333576Sgblack@eecs.umich.edu            elif action == "A":
1343576Sgblack@eecs.umich.edu                added.append(fname)
1353576Sgblack@eecs.umich.edu
1363576Sgblack@eecs.umich.edu        return added, modified
1373576Sgblack@eecs.umich.edu
1383576Sgblack@eecs.umich.edu    def staged_regions(self, fname, context=0):
1393576Sgblack@eecs.umich.edu        if self.file_status(fname, cached=True) in ("", "A", ):
1403576Sgblack@eecs.umich.edu            return all_regions
1413573Sgblack@eecs.umich.edu
1423573Sgblack@eecs.umich.edu        old = self.file_from_head(self.repo_path(fname)).split("\n")
1433573Sgblack@eecs.umich.edu        new = self.file_from_index(self.repo_path(fname)).split("\n")
1443573Sgblack@eecs.umich.edu
1457741Sgblack@eecs.umich.edu        return modified_regions(old, new, context=context)
1462221SN/A
1472221SN/A    def modified_regions(self, fname, context=0):
1487741Sgblack@eecs.umich.edu        if self.file_status(fname) in ("", "A", ):
1497741Sgblack@eecs.umich.edu            return all_regions
1503576Sgblack@eecs.umich.edu
1513576Sgblack@eecs.umich.edu        old = self.file_from_head(self.repo_path(fname)).split("\n")
1523576Sgblack@eecs.umich.edu        new = self.get_file(self.repo_path(fname)).split("\n")
1533576Sgblack@eecs.umich.edu
1543576Sgblack@eecs.umich.edu        return modified_regions(old, new, context=context)
1557741Sgblack@eecs.umich.edu
1563576Sgblack@eecs.umich.edu
1573576Sgblack@eecs.umich.edu    def head_revision(self):
1583576Sgblack@eecs.umich.edu        if self._head_revision is not None:
1593576Sgblack@eecs.umich.edu            return self._head_revision
1603576Sgblack@eecs.umich.edu
1613573Sgblack@eecs.umich.edu        try:
1623573Sgblack@eecs.umich.edu            self._head_revision = subprocess.check_output(
1632221SN/A                [ self.git, "rev-parse", "--verify", "HEAD" ],
1642221SN/A                stderr=subprocess.PIPE).rstrip("\n")
1657741Sgblack@eecs.umich.edu        except subprocess.CalledProcessError:
1662221SN/A            # Assume that the repo is empty and use the semi-magic
1672221SN/A            # empty tree revision if git rev-parse returned an error.
1683576Sgblack@eecs.umich.edu            self._head_revision = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1693576Sgblack@eecs.umich.edu
1703576Sgblack@eecs.umich.edu        return self._head_revision
1713576Sgblack@eecs.umich.edu
1723576Sgblack@eecs.umich.edu    def file_status(self, fname, cached=False):
1733576Sgblack@eecs.umich.edu        status = self.status(files=[fname], cached=cached)
1743576Sgblack@eecs.umich.edu        assert len(status) <= 1
1753576Sgblack@eecs.umich.edu        if status:
1763576Sgblack@eecs.umich.edu            return status[0][0]
1773576Sgblack@eecs.umich.edu        else:
1783576Sgblack@eecs.umich.edu            # No information available for the file. This usually
1793576Sgblack@eecs.umich.edu            # means that it hasn't been added to the
1803576Sgblack@eecs.umich.edu            # repository/commit.
1813576Sgblack@eecs.umich.edu            return ""
1823576Sgblack@eecs.umich.edu
1837741Sgblack@eecs.umich.edu    def status(self, filter=None, files=[], cached=False):
1843576Sgblack@eecs.umich.edu        cmd = [ self.git, "diff-index", "--name-status" ]
1853576Sgblack@eecs.umich.edu        if cached:
1863576Sgblack@eecs.umich.edu            cmd.append("--cached")
1873576Sgblack@eecs.umich.edu        if filter:
1883576Sgblack@eecs.umich.edu            cmd += [ "--diff-filter=%s" % filter ]
1893576Sgblack@eecs.umich.edu        cmd += [ self.head_revision(), "--" ] + files
1903576Sgblack@eecs.umich.edu        status = subprocess.check_output(cmd).rstrip("\n")
1913576Sgblack@eecs.umich.edu
1927741Sgblack@eecs.umich.edu        if status:
1933576Sgblack@eecs.umich.edu            return [ f.split("\t") for f in status.split("\n") ]
1943576Sgblack@eecs.umich.edu        else:
1953576Sgblack@eecs.umich.edu            return []
1963576Sgblack@eecs.umich.edu
1973576Sgblack@eecs.umich.edu    def file_from_index(self, name):
1983576Sgblack@eecs.umich.edu        return subprocess.check_output(
1997741Sgblack@eecs.umich.edu            [ self.git, "show", ":%s" % (name, ) ])
2003576Sgblack@eecs.umich.edu
2013576Sgblack@eecs.umich.edu    def file_from_head(self, name):
2023576Sgblack@eecs.umich.edu        return subprocess.check_output(
2033576Sgblack@eecs.umich.edu            [ self.git, "show", "%s:%s" % (self.head_revision(), name) ])
2043576Sgblack@eecs.umich.edu
2053576Sgblack@eecs.umich.educlass MercurialRepo(AbstractRepo):
2064103Ssaidi@eecs.umich.edu    def __init__(self):
2074103Ssaidi@eecs.umich.edu        self.hg = "hg"
2083576Sgblack@eecs.umich.edu        self._repo_base = None
2093576Sgblack@eecs.umich.edu
2103576Sgblack@eecs.umich.edu    def repo_base(self):
2113576Sgblack@eecs.umich.edu        if self._repo_base is None:
2123576Sgblack@eecs.umich.edu            self._repo_base = subprocess.check_output(
2134997Sgblack@eecs.umich.edu                [ self.hg, "root" ]).rstrip("\n")
2144997Sgblack@eecs.umich.edu
2154997Sgblack@eecs.umich.edu        return self._repo_base
2164997Sgblack@eecs.umich.edu
2174997Sgblack@eecs.umich.edu    def staged_files(self):
2184997Sgblack@eecs.umich.edu        added = []
2194997Sgblack@eecs.umich.edu        modified = []
2204997Sgblack@eecs.umich.edu        for action, fname in self.status():
2217678Sgblack@eecs.umich.edu            if action == "M":
2227678Sgblack@eecs.umich.edu                modified.append(fname)
2234997Sgblack@eecs.umich.edu            elif action == "A":
2244997Sgblack@eecs.umich.edu                added.append(fname)
2253576Sgblack@eecs.umich.edu
2264997Sgblack@eecs.umich.edu        return added, modified
2274997Sgblack@eecs.umich.edu
2284997Sgblack@eecs.umich.edu    def staged_regions(self, fname, context=0):
2294997Sgblack@eecs.umich.edu        return self.modified_regions(fname, context=context)
2304997Sgblack@eecs.umich.edu
2314997Sgblack@eecs.umich.edu    def modified_regions(self, fname, context=0):
2324997Sgblack@eecs.umich.edu        old = self.file_from_tip(fname).split("\n")
2334997Sgblack@eecs.umich.edu        new = self.get_file(fname).split("\n")
2347678Sgblack@eecs.umich.edu
2357678Sgblack@eecs.umich.edu        return modified_regions(old, new, context=context)
2364997Sgblack@eecs.umich.edu
2374997Sgblack@eecs.umich.edu    def status(self, filter=None):
2383576Sgblack@eecs.umich.edu        files = subprocess.check_output([ self.hg, "status" ]).rstrip("\n")
2393576Sgblack@eecs.umich.edu        if files:
2403576Sgblack@eecs.umich.edu            return [ f.split(" ") for f in files.split("\n") ]
2413576Sgblack@eecs.umich.edu        else:
2423576Sgblack@eecs.umich.edu            return []
2433576Sgblack@eecs.umich.edu
2443576Sgblack@eecs.umich.edu    def file_from_tip(self, name):
2453576Sgblack@eecs.umich.edu        return subprocess.check_output([ self.hg, "cat", name ])
2463576Sgblack@eecs.umich.edu
2473893Shsul@eecs.umich.edudef detect_repo(path="."):
2483576Sgblack@eecs.umich.edu    """Auto-detect the revision control system used for a source code
2493576Sgblack@eecs.umich.edu    directory. The code starts searching for repository meta data
2503576Sgblack@eecs.umich.edu    directories in path and then continues towards the root directory
2513576Sgblack@eecs.umich.edu    until root is reached or a metadatadirectory has been found.
2523576Sgblack@eecs.umich.edu
2537741Sgblack@eecs.umich.edu    Returns: List of repository helper classes that can interface with
2543576Sgblack@eecs.umich.edu    the detected revision control system(s).
2557678Sgblack@eecs.umich.edu
2567678Sgblack@eecs.umich.edu    """
2573576Sgblack@eecs.umich.edu
2583576Sgblack@eecs.umich.edu    _repo_types = (
2593576Sgblack@eecs.umich.edu        (".git", GitRepo),
2603576Sgblack@eecs.umich.edu        (".hg", MercurialRepo),
2613576Sgblack@eecs.umich.edu    )
2623576Sgblack@eecs.umich.edu
2637741Sgblack@eecs.umich.edu    repo_types = []
2647741Sgblack@eecs.umich.edu    for repo_dir, repo_class in _repo_types:
2653576Sgblack@eecs.umich.edu        if os.path.exists(os.path.join(path, repo_dir)):
2663576Sgblack@eecs.umich.edu            repo_types.append(repo_class)
2673576Sgblack@eecs.umich.edu
2683576Sgblack@eecs.umich.edu    if repo_types:
2693576Sgblack@eecs.umich.edu        return repo_types
2707741Sgblack@eecs.umich.edu    else:
2717741Sgblack@eecs.umich.edu        parent_dir = os.path.abspath(os.path.join(path, ".."))
2727741Sgblack@eecs.umich.edu        if not os.path.samefile(parent_dir, path):
2733576Sgblack@eecs.umich.edu            return detect_repo(path=parent_dir)
2747678Sgblack@eecs.umich.edu        else:
2757678Sgblack@eecs.umich.edu            # We reached the root directory without finding a meta
2763576Sgblack@eecs.umich.edu            # data directory.
2773576Sgblack@eecs.umich.edu            return []
2783576Sgblack@eecs.umich.edu