repo.py revision 11406
17008Snate@binkert.org#!/usr/bin/env python
27008Snate@binkert.org#
37008Snate@binkert.org# Copyright (c) 2016 ARM Limited
47008Snate@binkert.org# All rights reserved
57008Snate@binkert.org#
67008Snate@binkert.org# The license below extends only to copyright in the software and shall
77008Snate@binkert.org# not be construed as granting a license to any other intellectual
87008Snate@binkert.org# property including but not limited to intellectual property relating
97008Snate@binkert.org# to a hardware implementation of the functionality of the software
107008Snate@binkert.org# licensed hereunder.  You may use the software subject to the license
117008Snate@binkert.org# terms below provided that you ensure that this notice is replicated
127008Snate@binkert.org# unmodified and in its entirety in all distributions of the software,
137008Snate@binkert.org# modified or unmodified, in source code or in binary form.
147008Snate@binkert.org#
157008Snate@binkert.org# Redistribution and use in source and binary forms, with or without
167008Snate@binkert.org# modification, are permitted provided that the following conditions are
177008Snate@binkert.org# met: redistributions of source code must retain the above copyright
187008Snate@binkert.org# notice, this list of conditions and the following disclaimer;
197008Snate@binkert.org# redistributions in binary form must reproduce the above copyright
207008Snate@binkert.org# notice, this list of conditions and the following disclaimer in the
217008Snate@binkert.org# documentation and/or other materials provided with the distribution;
227008Snate@binkert.org# neither the name of the copyright holders nor the names of its
237008Snate@binkert.org# contributors may be used to endorse or promote products derived from
247008Snate@binkert.org# this software without specific prior written permission.
257008Snate@binkert.org#
267008Snate@binkert.org# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
277008Snate@binkert.org# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
286285Snate@binkert.org# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
297039Snate@binkert.org# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
307039Snate@binkert.org# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
316285Snate@binkert.org# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
3210706Spower.jg@gmail.com# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
336285Snate@binkert.org# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
347039Snate@binkert.org# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
359104Shestness@cs.utexas.edu# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
366285Snate@binkert.org# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3711339SMichael.Lebeane@amd.com#
386876Ssteve.reinhardt@amd.com# Authors: Andreas Sandberg
396876Ssteve.reinhardt@amd.com
407039Snate@binkert.orgfrom abc import *
417039Snate@binkert.orgimport os
427039Snate@binkert.orgimport subprocess
437039Snate@binkert.org
447039Snate@binkert.orgfrom region import *
457039Snate@binkert.orgfrom style import modified_regions
467039Snate@binkert.org
479208Snilay@cs.wisc.educlass AbstractRepo(object):
487039Snate@binkert.org    __metaclass__ = ABCMeta
496285Snate@binkert.org
506285Snate@binkert.org    def file_path(self, fname):
5111339SMichael.Lebeane@amd.com        """Get the absolute path to a file relative within the repository. The
527039Snate@binkert.org        input file name must be a valid path within the repository.
537039Snate@binkert.org
546876Ssteve.reinhardt@amd.com        """
557039Snate@binkert.org        return os.path.join(self.repo_base(), fname)
5611169Sandreas.hansson@arm.com
5710518Snilay@cs.wisc.edu    def in_repo(self, fname):
587039Snate@binkert.org        """Check if a path points to something within the repository base. Not
5911347Sandreas.hansson@arm.com        that this does not check for the presence of the object in the
607039Snate@binkert.org        file system as it could exist in the index without being in
6111347Sandreas.hansson@arm.com        the file system.
6211347Sandreas.hansson@arm.com
6311347Sandreas.hansson@arm.com        """
646285Snate@binkert.org        fname = os.path.abspath(fname)
657039Snate@binkert.org        repo_path = os.path.abspath(self.repo_base())
667039Snate@binkert.org
677039Snate@binkert.org        return os.path.commonprefix([repo_path, fname]) == repo_path
686285Snate@binkert.org
699104Shestness@cs.utexas.edu    def repo_path(self, fname):
709104Shestness@cs.utexas.edu        """Get the path of a file relative to the repository base. The input
717039Snate@binkert.org        file name is assumed to be an absolute path or a path relative
727039Snate@binkert.org        to the current working directory.
7310518Snilay@cs.wisc.edu
747039Snate@binkert.org        """
757039Snate@binkert.org        return os.path.relpath(fname, self.repo_base())
767039Snate@binkert.org
776285Snate@binkert.org    def get_file(self, name):
786285Snate@binkert.org        """Get the contents of a file in the file system using a path relative
797039Snate@binkert.org        to the repository root.
80
81        """
82        with open(self.file_path(name), "r") as f:
83            return f.read()
84
85    @abstractmethod
86    def repo_base(self):
87        """Get the path to the base of the repository"""
88        pass
89
90    @abstractmethod
91    def staged_files(self):
92        """Get a tuple describing the files that have been staged for a
93        commit: (list of new, list of modified)
94
95        """
96        pass
97
98    @abstractmethod
99    def staged_regions(self, fname, context=0):
100        """Get modified regions that will be committed by the next commit
101        command
102
103        """
104        pass
105
106    @abstractmethod
107    def modified_regions(self, fname, context=0):
108        """Get modified regions that have been staged for commit or are
109        present in the file system.
110
111        """
112        pass
113
114class GitRepo(AbstractRepo):
115    def __init__(self):
116        self.git = "git"
117        self._head_revision = None
118        self._repo_base = None
119
120    def repo_base(self):
121        if self._repo_base is None:
122            self._repo_base = subprocess.check_output(
123                [ self.git, "rev-parse", "--show-toplevel" ]).rstrip("\n")
124
125        return self._repo_base
126
127    def staged_files(self):
128        added = []
129        modified = []
130        for action, fname in self.status(filter="MA", cached=True):
131            if action == "M":
132                modified.append(fname)
133            elif action == "A":
134                added.append(fname)
135
136        return added, modified
137
138    def staged_regions(self, fname, context=0):
139        if self.file_status(fname, cached=True) in ("", "A", ):
140            return all_regions
141
142        old = self.file_from_head(self.repo_path(fname)).split("\n")
143        new = self.file_from_index(self.repo_path(fname)).split("\n")
144
145        return modified_regions(old, new, context=context)
146
147    def modified_regions(self, fname, context=0):
148        if self.file_status(fname) in ("", "A", ):
149            return all_regions
150
151        old = self.file_from_head(self.repo_path(fname)).split("\n")
152        new = self.get_file(self.repo_path(fname)).split("\n")
153
154        return modified_regions(old, new, context=context)
155
156
157    def head_revision(self):
158        if self._head_revision is not None:
159            return self._head_revision
160
161        try:
162            self._head_revision = subprocess.check_output(
163                [ self.git, "rev-parse", "--verify", "HEAD" ],
164                stderr=subprocess.PIPE).rstrip("\n")
165        except subprocess.CalledProcessError:
166            # Assume that the repo is empty and use the semi-magic
167            # empty tree revision if git rev-parse returned an error.
168            self._head_revision = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
169
170        return self._head_revision
171
172    def file_status(self, fname, cached=False):
173        status = self.status(files=[fname], cached=cached)
174        assert len(status) <= 1
175        if status:
176            return status[0][0]
177        else:
178            # No information available for the file. This usually
179            # means that it hasn't been added to the
180            # repository/commit.
181            return ""
182
183    def status(self, filter=None, files=[], cached=False):
184        cmd = [ self.git, "diff-index", "--name-status" ]
185        if cached:
186            cmd.append("--cached")
187        if filter:
188            cmd += [ "--diff-filter", filter ]
189        cmd += [ self.head_revision(), "--" ] + files
190        status = subprocess.check_output(cmd).rstrip("\n")
191
192        if status:
193            return [ f.split("\t") for f in status.split("\n") ]
194        else:
195            return []
196
197    def file_from_index(self, name):
198        return subprocess.check_output(
199            [ self.git, "show", ":%s" % (name, ) ])
200
201    def file_from_head(self, name):
202        return subprocess.check_output(
203            [ self.git, "show", "%s:%s" % (self.head_revision(), name) ])
204
205class MercurialRepo(AbstractRepo):
206    def __init__(self):
207        self.hg = "hg"
208        self._repo_base = None
209
210    def repo_base(self):
211        if self._repo_base is None:
212            self._repo_base = subprocess.check_output(
213                [ self.hg, "root" ]).rstrip("\n")
214
215        return self._repo_base
216
217    def staged_files(self):
218        added = []
219        modified = []
220        for action, fname in self.status():
221            if action == "M":
222                modified.append(fname)
223            elif action == "A":
224                added.append(fname)
225
226        return added, modified
227
228    def staged_regions(self, fname, context=0):
229        return self.modified_regions(fname, context=context)
230
231    def modified_regions(self, fname, context=0):
232        old = self.file_from_tip(fname).split("\n")
233        new = self.get_file(fname).split("\n")
234
235        return modified_regions(old, new, context=context)
236
237    def status(self, filter=None):
238        files = subprocess.check_output([ self.hg, "status" ]).rstrip("\n")
239        if files:
240            return [ f.split(" ") for f in files.split("\n") ]
241        else:
242            return []
243
244    def file_from_tip(self, name):
245        return subprocess.check_output([ self.hg, "cat", name ])
246
247def detect_repo(path="."):
248    """Auto-detect the revision control system used for a source code
249    directory. The code starts searching for repository meta data
250    directories in path and then continues towards the root directory
251    until root is reached or a metadatadirectory has been found.
252
253    Returns: List of repository helper classes that can interface with
254    the detected revision control system(s).
255
256    """
257
258    _repo_types = (
259        (".git", GitRepo),
260        (".hg", MercurialRepo),
261    )
262
263    repo_types = []
264    for repo_dir, repo_class in _repo_types:
265        if os.path.exists(os.path.join(path, repo_dir)):
266            repo_types.append(repo_class)
267
268    if repo_types:
269        return repo_types
270    else:
271        parent_dir = os.path.abspath(os.path.join(path, ".."))
272        if not os.path.samefile(parent_dir, path):
273            return detect_repo(path=parent_dir)
274        else:
275            # We reached the root directory without finding a meta
276            # data directory.
277            return []
278