Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: src/vcs.py

Issue 29599579: OffTopic: DependencyUpdater
Patch Set: Created Nov. 6, 2017, 2:04 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # This file is part of Adblock Plus <https://adblockplus.org/>,
2 # Copyright (C) 2006-present eyeo GmbH
3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation.
7 #
8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15
16 """VCS related classes for depup."""
17
18 from __future__ import print_function, unicode_literals
19
20 import io
21 import json
22 import os
23 import shutil
24 import subprocess
25 import sys
26 import tempfile
27
28
29 class Vcs(object):
30 """Baseclass for Git and Mercurial."""
31
32 JSON_DQUOTES = '__DQ__'
33
34 class VcsException(Exception):
35 """Raised when no distinct VCS for a given repository was found."""
36
37 def __init__(self, location):
38 """Construct a Vcs object for a given location.
39
40 parameters:
41 location: The repository location, may be a local folder or a remote
42 location.
43
44 When the specified location does not exist locally, Vcs will attempt
45 to create a temporary repository, cloned from the given location.
46
47 """
48 self._source, self._repository = os.path.split(location)
49 if not os.path.exists(location):
50 self._make_temporary(location)
51 self._clean_up = True
52 else:
53 self._cwd = location
54 self._clean_up = False
55
56 def __enter__(self):
57 """Enter the object's context."""
58 return self
59
60 def __exit__(self, exc_type, exc_value, traceback):
61 """Exit the object'c context and delete any temporary data."""
62 if self._clean_up:
63 shutil.rmtree(self._cwd)
64
65 @classmethod
66 def is_vcs_for_repo(cls, path):
67 """Assert if cls is a suitable VCS for the given (repository-) path."""
68 return os.path.exists(os.path.join(path, cls.VCS_REQUIREMENT))
69
70 def run_cmd(self, *args, **kwargs):
71 """Run the vcs with the given commands."""
72 cmd = self.BASE_CMD + args
73 try:
74 with open(os.devnull, 'w') as fpnull:
75 return subprocess.check_output(
76 cmd,
77 cwd=os.path.join(self._cwd),
78 stderr=fpnull,
79 ).decode('utf-8')
80 except subprocess.CalledProcessError as e:
81 print(e.output.decode('utf-8'), file=sys.stderr)
82 raise
83
84 def _get_latest(self):
85 self.run_cmd(self.UPDATE_LOCAL_HISTORY)
86
87 def _escape_changes(self, changes):
88 return changes.replace('"', '\\"').replace(self.JSON_DQUOTES, '"')
89
90 def _changes_as_json(self, changes):
91 return json.loads(
92 '[{}]'.format(','.join(
93 self._escape_changes(changes).strip().splitlines()
94 )))
95
96 def merged_diff(self, rev_a, rev_b, n_unified=16):
97 """Invoke the VCS' functionality to create a unified diff.
98
99 Parameters:
100 rev_a: The revision representing the start.
101 rev_b: The revision representing the end.
102 n_unified: The amount of context lines to add to the diff.
103
104 """
105 return self.run_cmd('diff', '--unified=' + str(n_unified),
106 *(self._rev_comb(rev_a, rev_b)))
107
108 def change_list(self, rev_a, rev_b):
109 """Return the repository's history from revisions a to b as JSON.
110
111 Parameters:
112 rev_a: The revision representing the start.
113 rev_b: The revision representing the end.
114
115 """
116 self._get_latest()
117
118 log_format = self._log_format()
119 rev_cmd = self._rev_comb(rev_a, rev_b)
120
121 changes = self.run_cmd(*('log',) + log_format + rev_cmd)
122 return self._changes_as_json(changes)
123
124 def enhance_changes_information(self, changes, dependency_location, fake):
125 """Enhance the change list with matching revisions from a mirror.
126
127 Parameters:
128 changes: The list to enhance, containing dictionaries
129 with the keys "hash", "author", "date" and
130 "message".
131 dependency_location: The (remote or locale) location of the
132 repository, which is supposed to be the mirror
133 for the current repository.
134 fake {True, False}: Causes atual processing of a mirror repository
135 (False) or fakes values (True)
136
137 """
138 self_ex = self.EXECUTABLE
139 mirr_ex = self._other_cls.EXECUTABLE
140
141 if not fake:
142 with self._other_cls(dependency_location) as mirror:
143 mirror._get_latest()
144
145 mirrored_hashes = {
146 change['hash']: mirror.matching_hash(change['author'],
147 change['date'],
148 change['message'])
149 for change in changes
150 }
151 else:
152 mirrored_hashes = {}
153
154 for change in changes:
155 change[self_ex + '_url'] = self.REVISION_URL.format(
156 repository=self._repository, revision=change['hash'])
157 change[self_ex + '_hash'] = change['hash']
158
159 mirrored_hash = mirrored_hashes.get(change['hash'], 'NO MIRROR')
160 del change['hash']
161
162 change[mirr_ex + '_url'] = self._other_cls.REVISION_URL.format(
163 repository=self._repository, revision=mirrored_hash)
164 change[mirr_ex + '_hash'] = mirrored_hash
165
166 @staticmethod
167 def factory(location):
168 """Get a suiting Vcs instance for the given repository path."""
169 obj = None
170 for cls in [Git, Mercurial]:
171 if cls.is_vcs_for_repo(location):
172 if obj is not None:
173 raise Vcs.VcsException(
174 "Found multiple possible VCS' for " + location)
175 obj = cls(location)
176
177 if obj is None:
178 raise Vcs.VcsException('No valid VCS found for ' + location)
179 return obj
180
181
182 class Mercurial(Vcs):
183 """Mercurial specialization of VCS."""
184
185 EXECUTABLE = 'hg'
186 VCS_REQUIREMENT = '.hg'
187 BASE_CMD = (EXECUTABLE, '--config', 'defaults.log=', '--config',
188 'defaults.pull=', '--config', 'defaults.diff=')
189 UPDATE_LOCAL_HISTORY = 'pull'
190 LOG_TEMLATE = ('\\{"hash":"{node|short}","author":"{author|person}",'
191 '"date":"{date|rfc822date}","message":"{desc|strip|'
192 'firstline}"}\n')
193
194 REVISION_URL = 'https://hg.adblockplus.org/{repository}/rev/{revision}'
195
196 def __init__(self, *args):
197 """Construct a Mercurial object and specify Git as the mirror class."""
198 self._other_cls = Git
199 super(Mercurial, self).__init__(*args)
200
201 def _rev_comb(self, rev_a, rev_b):
202 # Only take into account those changesets, which are actually affecting
203 # the repository's content. See
204 # https://www.mercurial-scm.org/repo/hg/help/revsets
205 return ('-r', '{}::{}'.format(rev_a, rev_b))
206
207 def _log_format(self):
208 log_format = self.LOG_TEMLATE.replace('"', self.JSON_DQUOTES)
209 return ('--template', log_format)
210
211 def change_list(self, *args):
212 """Apply measures for hg log and call Vcs's change_list."""
213 # Mercurial's command for producing a log between revisions using the
214 # revision set produced by self._rev_comb returns the changesets in a
215 # reversed order. Additionally the current revision is returned.
216 return list(reversed(super(Mercurial, self).change_list(*args)[1:]))
217
218 def matching_hash(self, author, date, message):
219 """Get the responsible commit for the given information.
220
221 A commit must stafisy equailty for auth, date and commit message, in
222 order to be recognized as the matching commit.
223
224 """
225 return self.run_cmd('log', '-u', author, '-d', date, '--keyword',
226 message, '--template', '{node|short}')
227
228 def _make_temporary(self, location):
229 self._cwd = tempfile.mkdtemp()
230 os.mkdir(os.path.join(self._cwd, '.hg'))
231
232 with io.open(os.path.join(self._cwd, '.hg', 'hgrc'), 'w') as fp:
233 fp.write('[paths]{}default = {}{}'.format(os.linesep, location,
234 os.linesep))
235
236
237 class Git(Vcs):
238 """Git specialization of Vcs."""
239
240 EXECUTABLE = 'git'
241 VCS_REQUIREMENT = '.git'
242 BASE_CMD = (EXECUTABLE,)
243 UPDATE_LOCAL_HISTORY = 'fetch'
244 LOG_TEMLATE = '{"hash":"%h","author":"%an","date":"%aD","message":"%s"}'
245
246 REVISION_URL = ('https://www.github.com/adblockplus/{repository}/commit/'
247 '{revision}')
248
249 def __init__(self, *args):
250 """Construct a Git object and specify Mercurial as the mirror class."""
251 self._other_cls = Mercurial
252 super(Git, self).__init__(*args)
253
254 def _rev_comb(self, rev_a, rev_b):
255 return ('{}..{}'.format(rev_a, rev_b),)
256
257 def _log_format(self):
258 return ('--pretty=format:{}'.format(self.LOG_TEMLATE.replace(
259 '"', self.JSON_DQUOTES)),)
260
261 def matching_hash(self, author, date, message):
262 """Get the responsible commit for the given information.
263
264 A commit must stafisy equailty for auth, date and commit message, in
265 order to be recognized as the matching commit.
266
267 """
268 # Git does not implement exact date matching directly, therefore we
269 # need to filter for "not before" and "not after" the given time.
270 return self.run_cmd('log', '--author={}'.format(author),
271 '--grep={}'.format(message), '--not',
272 '--before={}'.format(date), '--not',
273 '--after={}'.format(date), '--pretty=format:%h')
274
275 def _make_temporary(self, location):
276 self._cwd = tempfile.mkdtemp()
277 self.run_cmd('clone', '--bare', location, self._cwd)
OLDNEW
« src/templates/default.trac ('K') | « src/templates/default.trac ('k') | tox.ini » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld