##// END OF EJS Templates
model: simplify how get_commits_stats task group on author...
Mads Kiilerich -
r8595:4a18e6bf default
parent child Browse files
Show More
@@ -1,509 +1,527 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.async_tasks
15 kallithea.model.async_tasks
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Kallithea task modules, containing all task that suppose to be run
18 Kallithea task modules, containing all task that suppose to be run
19 by celery daemon
19 by celery daemon
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Oct 6, 2010
23 :created_on: Oct 6, 2010
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29 import email.message
29 import email.message
30 import email.utils
30 import email.utils
31 import os
31 import os
32 import smtplib
32 import smtplib
33 import time
33 import time
34 import traceback
34 import traceback
35 from collections import OrderedDict
35 from collections import OrderedDict
36 from operator import itemgetter
36 from operator import itemgetter
37 from time import mktime
37 from time import mktime
38
38
39 import celery.utils.log
39 import celery.utils.log
40 from tg import config
40 from tg import config
41
41
42 import kallithea
42 import kallithea
43 import kallithea.lib.helpers as h
44 from kallithea.lib import celerylib, conf, ext_json, hooks
43 from kallithea.lib import celerylib, conf, ext_json, hooks
45 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
44 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
46 from kallithea.lib.utils2 import asbool, ascii_bytes
45 from kallithea.lib.utils2 import asbool, ascii_bytes
47 from kallithea.lib.vcs.utils import author_email
46 from kallithea.lib.vcs.utils import author_email, author_name
48 from kallithea.model import db, repo, userlog
47 from kallithea.model import db, repo, userlog
49
48
50
49
51 __all__ = ['whoosh_index', 'get_commits_stats', 'send_email']
50 __all__ = ['whoosh_index', 'get_commits_stats', 'send_email']
52
51
53
52
54 log = celery.utils.log.get_task_logger(__name__)
53 log = celery.utils.log.get_task_logger(__name__)
55
54
56
55
57 @celerylib.task
56 @celerylib.task
58 @celerylib.locked_task
57 @celerylib.locked_task
59 @celerylib.dbsession
58 @celerylib.dbsession
60 def whoosh_index(repo_location, full_index):
59 def whoosh_index(repo_location, full_index):
61 celerylib.get_session() # initialize database connection
60 celerylib.get_session() # initialize database connection
62
61
63 index_location = config['index_dir']
62 index_location = config['index_dir']
64 WhooshIndexingDaemon(index_location=index_location,
63 WhooshIndexingDaemon(index_location=index_location,
65 repo_location=repo_location) \
64 repo_location=repo_location) \
66 .run(full_index=full_index)
65 .run(full_index=full_index)
67
66
68
67
68 def _author_username(author):
69 """Return the username of the user identified by the email part of the 'author' string,
70 default to the name or email.
71 Kind of similar to h.person() ."""
72 email = author_email(author)
73 if email:
74 user = db.User.get_by_email(email)
75 if user is not None:
76 return user.username
77 # Still nothing? Just pass back the author name if any, else the email
78 return author_name(author) or email
79
80
69 @celerylib.task
81 @celerylib.task
70 @celerylib.dbsession
82 @celerylib.dbsession
71 def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100):
83 def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100):
72 DBS = celerylib.get_session()
84 DBS = celerylib.get_session()
73 lockkey = celerylib.__get_lockkey('get_commits_stats', repo_name, ts_min_y,
85 lockkey = celerylib.__get_lockkey('get_commits_stats', repo_name, ts_min_y,
74 ts_max_y)
86 ts_max_y)
75 lockkey_path = config.get('cache_dir') or config['app_conf']['cache_dir'] # Backward compatibility for TurboGears < 2.4
87 lockkey_path = config.get('cache_dir') or config['app_conf']['cache_dir'] # Backward compatibility for TurboGears < 2.4
76
88
77 log.info('running task with lockkey %s', lockkey)
89 log.info('running task with lockkey %s', lockkey)
78
90
79 try:
91 try:
80 lock = celerylib.DaemonLock(os.path.join(lockkey_path, lockkey))
92 lock = celerylib.DaemonLock(os.path.join(lockkey_path, lockkey))
81
93
82 co_day_auth_aggr = {}
94 co_day_auth_aggr = {}
83 commits_by_day_aggregate = {}
95 commits_by_day_aggregate = {}
84 db_repo = db.Repository.get_by_repo_name(repo_name)
96 db_repo = db.Repository.get_by_repo_name(repo_name)
85 if db_repo is None:
97 if db_repo is None:
86 return True
98 return True
87
99
88 scm_repo = db_repo.scm_instance
100 scm_repo = db_repo.scm_instance
89 repo_size = scm_repo.count()
101 repo_size = scm_repo.count()
90 # return if repo have no revisions
102 # return if repo have no revisions
91 if repo_size < 1:
103 if repo_size < 1:
92 lock.release()
104 lock.release()
93 return True
105 return True
94
106
95 skip_date_limit = True
107 skip_date_limit = True
96 parse_limit = int(config.get('commit_parse_limit'))
108 parse_limit = int(config.get('commit_parse_limit'))
97 last_rev = None
109 last_rev = None
98 last_cs = None
110 last_cs = None
99 timegetter = itemgetter('time')
111 timegetter = itemgetter('time')
100
112
101 dbrepo = DBS.query(db.Repository) \
113 dbrepo = DBS.query(db.Repository) \
102 .filter(db.Repository.repo_name == repo_name).scalar()
114 .filter(db.Repository.repo_name == repo_name).scalar()
103 cur_stats = DBS.query(db.Statistics) \
115 cur_stats = DBS.query(db.Statistics) \
104 .filter(db.Statistics.repository == dbrepo).scalar()
116 .filter(db.Statistics.repository == dbrepo).scalar()
105
117
106 if cur_stats is not None:
118 if cur_stats is not None:
107 last_rev = cur_stats.stat_on_revision
119 last_rev = cur_stats.stat_on_revision
108
120
109 if last_rev == scm_repo.get_changeset().revision and repo_size > 1:
121 if last_rev == scm_repo.get_changeset().revision and repo_size > 1:
110 # pass silently without any work if we're not on first revision or
122 # pass silently without any work if we're not on first revision or
111 # current state of parsing revision(from db marker) is the
123 # current state of parsing revision(from db marker) is the
112 # last revision
124 # last revision
113 lock.release()
125 lock.release()
114 return True
126 return True
115
127
116 if cur_stats:
128 if cur_stats:
117 commits_by_day_aggregate = OrderedDict(ext_json.loads(
129 commits_by_day_aggregate = OrderedDict(ext_json.loads(
118 cur_stats.commit_activity_combined))
130 cur_stats.commit_activity_combined))
119 co_day_auth_aggr = ext_json.loads(cur_stats.commit_activity)
131 co_day_auth_aggr = ext_json.loads(cur_stats.commit_activity)
120
132
121 log.debug('starting parsing %s', parse_limit)
133 log.debug('starting parsing %s', parse_limit)
122
134
123 last_rev = last_rev + 1 if last_rev and last_rev >= 0 else 0
135 last_rev = last_rev + 1 if last_rev and last_rev >= 0 else 0
124 log.debug('Getting revisions from %s to %s',
136 log.debug('Getting revisions from %s to %s',
125 last_rev, last_rev + parse_limit
137 last_rev, last_rev + parse_limit
126 )
138 )
139 usernames_cache = {}
127 for cs in scm_repo[last_rev:last_rev + parse_limit]:
140 for cs in scm_repo[last_rev:last_rev + parse_limit]:
128 log.debug('parsing %s', cs)
141 log.debug('parsing %s', cs)
129 last_cs = cs # remember last parsed changeset
142 last_cs = cs # remember last parsed changeset
130 tt = cs.date.timetuple()
143 tt = cs.date.timetuple()
131 k = mktime(tt[:3] + (0, 0, 0, 0, 0, 0))
144 k = mktime(tt[:3] + (0, 0, 0, 0, 0, 0))
132
145
133 username = h.person(cs.author)
146 # get username from author - similar to what h.person does
147 username = usernames_cache.get(cs.author)
148 if username is None:
149 username = _author_username(cs.author)
150 usernames_cache[cs.author] = username
151
134 if username in co_day_auth_aggr:
152 if username in co_day_auth_aggr:
135 try:
153 try:
136 l = [timegetter(x) for x in
154 l = [timegetter(x) for x in
137 co_day_auth_aggr[username]['data']]
155 co_day_auth_aggr[username]['data']]
138 time_pos = l.index(k)
156 time_pos = l.index(k)
139 except ValueError:
157 except ValueError:
140 time_pos = None
158 time_pos = None
141
159
142 if time_pos is not None and time_pos >= 0:
160 if time_pos is not None and time_pos >= 0:
143 datadict = \
161 datadict = \
144 co_day_auth_aggr[username]['data'][time_pos]
162 co_day_auth_aggr[username]['data'][time_pos]
145
163
146 datadict["commits"] += 1
164 datadict["commits"] += 1
147 datadict["added"] += len(cs.added)
165 datadict["added"] += len(cs.added)
148 datadict["changed"] += len(cs.changed)
166 datadict["changed"] += len(cs.changed)
149 datadict["removed"] += len(cs.removed)
167 datadict["removed"] += len(cs.removed)
150
168
151 else:
169 else:
152 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
170 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
153
171
154 datadict = {"time": k,
172 datadict = {"time": k,
155 "commits": 1,
173 "commits": 1,
156 "added": len(cs.added),
174 "added": len(cs.added),
157 "changed": len(cs.changed),
175 "changed": len(cs.changed),
158 "removed": len(cs.removed),
176 "removed": len(cs.removed),
159 }
177 }
160 co_day_auth_aggr[username]['data'] \
178 co_day_auth_aggr[username]['data'] \
161 .append(datadict)
179 .append(datadict)
162
180
163 else:
181 else:
164 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
182 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
165 co_day_auth_aggr[username] = {
183 co_day_auth_aggr[username] = {
166 "label": username,
184 "label": username,
167 "data": [{"time": k,
185 "data": [{"time": k,
168 "commits": 1,
186 "commits": 1,
169 "added": len(cs.added),
187 "added": len(cs.added),
170 "changed": len(cs.changed),
188 "changed": len(cs.changed),
171 "removed": len(cs.removed),
189 "removed": len(cs.removed),
172 }],
190 }],
173 "schema": ["commits"],
191 "schema": ["commits"],
174 }
192 }
175
193
176 # gather all data by day
194 # gather all data by day
177 if k in commits_by_day_aggregate:
195 if k in commits_by_day_aggregate:
178 commits_by_day_aggregate[k] += 1
196 commits_by_day_aggregate[k] += 1
179 else:
197 else:
180 commits_by_day_aggregate[k] = 1
198 commits_by_day_aggregate[k] = 1
181
199
182 overview_data = sorted(commits_by_day_aggregate.items(),
200 overview_data = sorted(commits_by_day_aggregate.items(),
183 key=itemgetter(0))
201 key=itemgetter(0))
184
202
185 stats = cur_stats if cur_stats else db.Statistics()
203 stats = cur_stats if cur_stats else db.Statistics()
186 stats.commit_activity = ascii_bytes(ext_json.dumps(co_day_auth_aggr))
204 stats.commit_activity = ascii_bytes(ext_json.dumps(co_day_auth_aggr))
187 stats.commit_activity_combined = ascii_bytes(ext_json.dumps(overview_data))
205 stats.commit_activity_combined = ascii_bytes(ext_json.dumps(overview_data))
188
206
189 log.debug('last revision %s', last_rev)
207 log.debug('last revision %s', last_rev)
190 leftovers = len(scm_repo.revisions[last_rev:])
208 leftovers = len(scm_repo.revisions[last_rev:])
191 log.debug('revisions to parse %s', leftovers)
209 log.debug('revisions to parse %s', leftovers)
192
210
193 if last_rev == 0 or leftovers < parse_limit:
211 if last_rev == 0 or leftovers < parse_limit:
194 log.debug('getting code trending stats')
212 log.debug('getting code trending stats')
195 stats.languages = ascii_bytes(ext_json.dumps(__get_codes_stats(repo_name)))
213 stats.languages = ascii_bytes(ext_json.dumps(__get_codes_stats(repo_name)))
196
214
197 try:
215 try:
198 stats.repository = dbrepo
216 stats.repository = dbrepo
199 stats.stat_on_revision = last_cs.revision if last_cs else 0
217 stats.stat_on_revision = last_cs.revision if last_cs else 0
200 DBS.add(stats)
218 DBS.add(stats)
201 DBS.commit()
219 DBS.commit()
202 except:
220 except:
203 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
204 DBS.rollback()
222 DBS.rollback()
205 lock.release()
223 lock.release()
206 return False
224 return False
207
225
208 # final release
226 # final release
209 lock.release()
227 lock.release()
210
228
211 # execute another task if celery is enabled
229 # execute another task if celery is enabled
212 if len(scm_repo.revisions) > 1 and kallithea.CELERY_APP and recurse_limit > 0:
230 if len(scm_repo.revisions) > 1 and kallithea.CELERY_APP and recurse_limit > 0:
213 get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit - 1)
231 get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit - 1)
214 elif recurse_limit <= 0:
232 elif recurse_limit <= 0:
215 log.debug('Not recursing - limit has been reached')
233 log.debug('Not recursing - limit has been reached')
216 else:
234 else:
217 log.debug('Not recursing')
235 log.debug('Not recursing')
218 except celerylib.LockHeld:
236 except celerylib.LockHeld:
219 log.info('Task with key %s already running', lockkey)
237 log.info('Task with key %s already running', lockkey)
220 return 'Task with key %s already running' % lockkey
238 return 'Task with key %s already running' % lockkey
221
239
222
240
223 @celerylib.task
241 @celerylib.task
224 @celerylib.dbsession
242 @celerylib.dbsession
225 def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
243 def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
226 """
244 """
227 Sends an email with defined parameters from the .ini files.
245 Sends an email with defined parameters from the .ini files.
228
246
229 :param recipients: list of recipients, if this is None, the defined email
247 :param recipients: list of recipients, if this is None, the defined email
230 address from field 'email_to' and all admins is used instead
248 address from field 'email_to' and all admins is used instead
231 :param subject: subject of the mail
249 :param subject: subject of the mail
232 :param body: plain text body of the mail
250 :param body: plain text body of the mail
233 :param html_body: html version of body
251 :param html_body: html version of body
234 :param headers: dictionary of prepopulated e-mail headers
252 :param headers: dictionary of prepopulated e-mail headers
235 :param from_name: full name to be used as sender of this mail - often a
253 :param from_name: full name to be used as sender of this mail - often a
236 .full_name_or_username value
254 .full_name_or_username value
237 """
255 """
238 assert isinstance(recipients, list), recipients
256 assert isinstance(recipients, list), recipients
239 if headers is None:
257 if headers is None:
240 headers = {}
258 headers = {}
241 else:
259 else:
242 # do not modify the original headers object passed by the caller
260 # do not modify the original headers object passed by the caller
243 headers = headers.copy()
261 headers = headers.copy()
244
262
245 email_config = config
263 email_config = config
246 email_prefix = email_config.get('email_prefix', '')
264 email_prefix = email_config.get('email_prefix', '')
247 if email_prefix:
265 if email_prefix:
248 subject = "%s %s" % (email_prefix, subject)
266 subject = "%s %s" % (email_prefix, subject)
249
267
250 if not recipients:
268 if not recipients:
251 # if recipients are not defined we send to email_config + all admins
269 # if recipients are not defined we send to email_config + all admins
252 recipients = [u.email for u in db.User.query()
270 recipients = [u.email for u in db.User.query()
253 .filter(db.User.admin == True).all()]
271 .filter(db.User.admin == True).all()]
254 if email_config.get('email_to') is not None:
272 if email_config.get('email_to') is not None:
255 recipients += email_config.get('email_to').split(',')
273 recipients += email_config.get('email_to').split(',')
256
274
257 # If there are still no recipients, there are no admins and no address
275 # If there are still no recipients, there are no admins and no address
258 # configured in email_to, so return.
276 # configured in email_to, so return.
259 if not recipients:
277 if not recipients:
260 log.error("No recipients specified and no fallback available.")
278 log.error("No recipients specified and no fallback available.")
261 return False
279 return False
262
280
263 log.warning("No recipients specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
281 log.warning("No recipients specified for '%s' - sending to admins %s", subject, ' '.join(recipients))
264
282
265 # SMTP sender
283 # SMTP sender
266 app_email_from = email_config.get('app_email_from', 'Kallithea')
284 app_email_from = email_config.get('app_email_from', 'Kallithea')
267 # 'From' header
285 # 'From' header
268 if from_name is not None:
286 if from_name is not None:
269 # set From header based on from_name but with a generic e-mail address
287 # set From header based on from_name but with a generic e-mail address
270 # In case app_email_from is in "Some Name <e-mail>" format, we first
288 # In case app_email_from is in "Some Name <e-mail>" format, we first
271 # extract the e-mail address.
289 # extract the e-mail address.
272 envelope_addr = author_email(app_email_from)
290 envelope_addr = author_email(app_email_from)
273 headers['From'] = '"%s" <%s>' % (
291 headers['From'] = '"%s" <%s>' % (
274 email.utils.quote('%s (no-reply)' % from_name),
292 email.utils.quote('%s (no-reply)' % from_name),
275 envelope_addr)
293 envelope_addr)
276
294
277 smtp_server = email_config.get('smtp_server')
295 smtp_server = email_config.get('smtp_server')
278 smtp_port = email_config.get('smtp_port')
296 smtp_port = email_config.get('smtp_port')
279 smtp_use_tls = asbool(email_config.get('smtp_use_tls'))
297 smtp_use_tls = asbool(email_config.get('smtp_use_tls'))
280 smtp_use_ssl = asbool(email_config.get('smtp_use_ssl'))
298 smtp_use_ssl = asbool(email_config.get('smtp_use_ssl'))
281 smtp_auth = email_config.get('smtp_auth') # undocumented - overrule automatic choice of auth mechanism
299 smtp_auth = email_config.get('smtp_auth') # undocumented - overrule automatic choice of auth mechanism
282 smtp_username = email_config.get('smtp_username')
300 smtp_username = email_config.get('smtp_username')
283 smtp_password = email_config.get('smtp_password')
301 smtp_password = email_config.get('smtp_password')
284
302
285 logmsg = ("Mail details:\n"
303 logmsg = ("Mail details:\n"
286 "recipients: %s\n"
304 "recipients: %s\n"
287 "headers: %s\n"
305 "headers: %s\n"
288 "subject: %s\n"
306 "subject: %s\n"
289 "body:\n%s\n"
307 "body:\n%s\n"
290 "html:\n%s\n"
308 "html:\n%s\n"
291 % (' '.join(recipients), headers, subject, body, html_body))
309 % (' '.join(recipients), headers, subject, body, html_body))
292
310
293 if smtp_server:
311 if smtp_server:
294 log.debug("Sending e-mail. " + logmsg)
312 log.debug("Sending e-mail. " + logmsg)
295 else:
313 else:
296 log.error("SMTP mail server not configured - cannot send e-mail.")
314 log.error("SMTP mail server not configured - cannot send e-mail.")
297 log.warning(logmsg)
315 log.warning(logmsg)
298 return False
316 return False
299
317
300 msg = email.message.EmailMessage()
318 msg = email.message.EmailMessage()
301 msg['Subject'] = subject
319 msg['Subject'] = subject
302 msg['From'] = app_email_from # fallback - might be overridden by a header
320 msg['From'] = app_email_from # fallback - might be overridden by a header
303 msg['To'] = ', '.join(recipients)
321 msg['To'] = ', '.join(recipients)
304 msg['Date'] = email.utils.formatdate(time.time())
322 msg['Date'] = email.utils.formatdate(time.time())
305
323
306 for key, value in headers.items():
324 for key, value in headers.items():
307 del msg[key] # Delete key first to make sure add_header will replace header (if any), no matter the casing
325 del msg[key] # Delete key first to make sure add_header will replace header (if any), no matter the casing
308 msg.add_header(key, value)
326 msg.add_header(key, value)
309
327
310 msg.set_content(body)
328 msg.set_content(body)
311 msg.add_alternative(html_body, subtype='html')
329 msg.add_alternative(html_body, subtype='html')
312
330
313 try:
331 try:
314 if smtp_use_ssl:
332 if smtp_use_ssl:
315 smtp_serv = smtplib.SMTP_SSL(smtp_server, smtp_port)
333 smtp_serv = smtplib.SMTP_SSL(smtp_server, smtp_port)
316 else:
334 else:
317 smtp_serv = smtplib.SMTP(smtp_server, smtp_port)
335 smtp_serv = smtplib.SMTP(smtp_server, smtp_port)
318
336
319 if smtp_use_tls:
337 if smtp_use_tls:
320 smtp_serv.starttls()
338 smtp_serv.starttls()
321
339
322 if smtp_auth:
340 if smtp_auth:
323 smtp_serv.ehlo() # populate esmtp_features
341 smtp_serv.ehlo() # populate esmtp_features
324 smtp_serv.esmtp_features["auth"] = smtp_auth
342 smtp_serv.esmtp_features["auth"] = smtp_auth
325
343
326 if smtp_username and smtp_password is not None:
344 if smtp_username and smtp_password is not None:
327 smtp_serv.login(smtp_username, smtp_password)
345 smtp_serv.login(smtp_username, smtp_password)
328
346
329 smtp_serv.sendmail(app_email_from, recipients, msg.as_string())
347 smtp_serv.sendmail(app_email_from, recipients, msg.as_string())
330 smtp_serv.quit()
348 smtp_serv.quit()
331
349
332 log.info('Mail was sent to: %s' % recipients)
350 log.info('Mail was sent to: %s' % recipients)
333 except:
351 except:
334 log.error('Mail sending failed')
352 log.error('Mail sending failed')
335 log.error(traceback.format_exc())
353 log.error(traceback.format_exc())
336 return False
354 return False
337 return True
355 return True
338
356
339
357
340 @celerylib.task
358 @celerylib.task
341 @celerylib.dbsession
359 @celerylib.dbsession
342 def create_repo(form_data, cur_user):
360 def create_repo(form_data, cur_user):
343 DBS = celerylib.get_session()
361 DBS = celerylib.get_session()
344
362
345 cur_user = db.User.guess_instance(cur_user)
363 cur_user = db.User.guess_instance(cur_user)
346
364
347 owner = cur_user
365 owner = cur_user
348 repo_name = form_data['repo_name']
366 repo_name = form_data['repo_name']
349 repo_name_full = form_data['repo_name_full']
367 repo_name_full = form_data['repo_name_full']
350 repo_type = form_data['repo_type']
368 repo_type = form_data['repo_type']
351 description = form_data['repo_description']
369 description = form_data['repo_description']
352 private = form_data['repo_private']
370 private = form_data['repo_private']
353 clone_uri = form_data.get('clone_uri')
371 clone_uri = form_data.get('clone_uri')
354 repo_group = form_data['repo_group']
372 repo_group = form_data['repo_group']
355 landing_rev = form_data['repo_landing_rev']
373 landing_rev = form_data['repo_landing_rev']
356 copy_fork_permissions = form_data.get('copy_permissions')
374 copy_fork_permissions = form_data.get('copy_permissions')
357 copy_group_permissions = form_data.get('repo_copy_permissions')
375 copy_group_permissions = form_data.get('repo_copy_permissions')
358 fork_of = form_data.get('fork_parent_id')
376 fork_of = form_data.get('fork_parent_id')
359 state = form_data.get('repo_state', db.Repository.STATE_PENDING)
377 state = form_data.get('repo_state', db.Repository.STATE_PENDING)
360
378
361 # repo creation defaults, private and repo_type are filled in form
379 # repo creation defaults, private and repo_type are filled in form
362 defs = db.Setting.get_default_repo_settings(strip_prefix=True)
380 defs = db.Setting.get_default_repo_settings(strip_prefix=True)
363 enable_statistics = defs.get('repo_enable_statistics')
381 enable_statistics = defs.get('repo_enable_statistics')
364 enable_downloads = defs.get('repo_enable_downloads')
382 enable_downloads = defs.get('repo_enable_downloads')
365
383
366 try:
384 try:
367 db_repo = repo.RepoModel()._create_repo(
385 db_repo = repo.RepoModel()._create_repo(
368 repo_name=repo_name_full,
386 repo_name=repo_name_full,
369 repo_type=repo_type,
387 repo_type=repo_type,
370 description=description,
388 description=description,
371 owner=owner,
389 owner=owner,
372 private=private,
390 private=private,
373 clone_uri=clone_uri,
391 clone_uri=clone_uri,
374 repo_group=repo_group,
392 repo_group=repo_group,
375 landing_rev=landing_rev,
393 landing_rev=landing_rev,
376 fork_of=fork_of,
394 fork_of=fork_of,
377 copy_fork_permissions=copy_fork_permissions,
395 copy_fork_permissions=copy_fork_permissions,
378 copy_group_permissions=copy_group_permissions,
396 copy_group_permissions=copy_group_permissions,
379 enable_statistics=enable_statistics,
397 enable_statistics=enable_statistics,
380 enable_downloads=enable_downloads,
398 enable_downloads=enable_downloads,
381 state=state
399 state=state
382 )
400 )
383
401
384 userlog.action_logger(cur_user, 'user_created_repo',
402 userlog.action_logger(cur_user, 'user_created_repo',
385 form_data['repo_name_full'], '')
403 form_data['repo_name_full'], '')
386
404
387 DBS.commit()
405 DBS.commit()
388 # now create this repo on Filesystem
406 # now create this repo on Filesystem
389 repo.RepoModel()._create_filesystem_repo(
407 repo.RepoModel()._create_filesystem_repo(
390 repo_name=repo_name,
408 repo_name=repo_name,
391 repo_type=repo_type,
409 repo_type=repo_type,
392 repo_group=db.RepoGroup.guess_instance(repo_group),
410 repo_group=db.RepoGroup.guess_instance(repo_group),
393 clone_uri=clone_uri,
411 clone_uri=clone_uri,
394 )
412 )
395 db_repo = db.Repository.get_by_repo_name(repo_name_full)
413 db_repo = db.Repository.get_by_repo_name(repo_name_full)
396 hooks.log_create_repository(db_repo.get_dict(), created_by=owner.username)
414 hooks.log_create_repository(db_repo.get_dict(), created_by=owner.username)
397
415
398 # update repo changeset caches initially
416 # update repo changeset caches initially
399 db_repo.update_changeset_cache()
417 db_repo.update_changeset_cache()
400
418
401 # set new created state
419 # set new created state
402 db_repo.set_state(db.Repository.STATE_CREATED)
420 db_repo.set_state(db.Repository.STATE_CREATED)
403 DBS.commit()
421 DBS.commit()
404 except Exception as e:
422 except Exception as e:
405 log.warning('Exception %s occurred when forking repository, '
423 log.warning('Exception %s occurred when forking repository, '
406 'doing cleanup...' % e)
424 'doing cleanup...' % e)
407 # rollback things manually !
425 # rollback things manually !
408 db_repo = db.Repository.get_by_repo_name(repo_name_full)
426 db_repo = db.Repository.get_by_repo_name(repo_name_full)
409 if db_repo:
427 if db_repo:
410 db.Repository.delete(db_repo.repo_id)
428 db.Repository.delete(db_repo.repo_id)
411 DBS.commit()
429 DBS.commit()
412 repo.RepoModel()._delete_filesystem_repo(db_repo)
430 repo.RepoModel()._delete_filesystem_repo(db_repo)
413 raise
431 raise
414
432
415 return True
433 return True
416
434
417
435
418 @celerylib.task
436 @celerylib.task
419 @celerylib.dbsession
437 @celerylib.dbsession
420 def create_repo_fork(form_data, cur_user):
438 def create_repo_fork(form_data, cur_user):
421 """
439 """
422 Creates a fork of repository using interval VCS methods
440 Creates a fork of repository using interval VCS methods
423
441
424 :param form_data:
442 :param form_data:
425 :param cur_user:
443 :param cur_user:
426 """
444 """
427 DBS = celerylib.get_session()
445 DBS = celerylib.get_session()
428
446
429 base_path = kallithea.CONFIG['base_path']
447 base_path = kallithea.CONFIG['base_path']
430 cur_user = db.User.guess_instance(cur_user)
448 cur_user = db.User.guess_instance(cur_user)
431
449
432 repo_name = form_data['repo_name'] # fork in this case
450 repo_name = form_data['repo_name'] # fork in this case
433 repo_name_full = form_data['repo_name_full']
451 repo_name_full = form_data['repo_name_full']
434
452
435 repo_type = form_data['repo_type']
453 repo_type = form_data['repo_type']
436 owner = cur_user
454 owner = cur_user
437 private = form_data['private']
455 private = form_data['private']
438 clone_uri = form_data.get('clone_uri')
456 clone_uri = form_data.get('clone_uri')
439 repo_group = form_data['repo_group']
457 repo_group = form_data['repo_group']
440 landing_rev = form_data['landing_rev']
458 landing_rev = form_data['landing_rev']
441 copy_fork_permissions = form_data.get('copy_permissions')
459 copy_fork_permissions = form_data.get('copy_permissions')
442
460
443 try:
461 try:
444 fork_of = db.Repository.guess_instance(form_data.get('fork_parent_id'))
462 fork_of = db.Repository.guess_instance(form_data.get('fork_parent_id'))
445
463
446 repo.RepoModel()._create_repo(
464 repo.RepoModel()._create_repo(
447 repo_name=repo_name_full,
465 repo_name=repo_name_full,
448 repo_type=repo_type,
466 repo_type=repo_type,
449 description=form_data['description'],
467 description=form_data['description'],
450 owner=owner,
468 owner=owner,
451 private=private,
469 private=private,
452 clone_uri=clone_uri,
470 clone_uri=clone_uri,
453 repo_group=repo_group,
471 repo_group=repo_group,
454 landing_rev=landing_rev,
472 landing_rev=landing_rev,
455 fork_of=fork_of,
473 fork_of=fork_of,
456 copy_fork_permissions=copy_fork_permissions
474 copy_fork_permissions=copy_fork_permissions
457 )
475 )
458 userlog.action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full,
476 userlog.action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full,
459 fork_of.repo_name, '')
477 fork_of.repo_name, '')
460 DBS.commit()
478 DBS.commit()
461
479
462 source_repo_path = os.path.join(base_path, fork_of.repo_name)
480 source_repo_path = os.path.join(base_path, fork_of.repo_name)
463
481
464 # now create this repo on Filesystem
482 # now create this repo on Filesystem
465 repo.RepoModel()._create_filesystem_repo(
483 repo.RepoModel()._create_filesystem_repo(
466 repo_name=repo_name,
484 repo_name=repo_name,
467 repo_type=repo_type,
485 repo_type=repo_type,
468 repo_group=db.RepoGroup.guess_instance(repo_group),
486 repo_group=db.RepoGroup.guess_instance(repo_group),
469 clone_uri=source_repo_path,
487 clone_uri=source_repo_path,
470 )
488 )
471 db_repo = db.Repository.get_by_repo_name(repo_name_full)
489 db_repo = db.Repository.get_by_repo_name(repo_name_full)
472 hooks.log_create_repository(db_repo.get_dict(), created_by=owner.username)
490 hooks.log_create_repository(db_repo.get_dict(), created_by=owner.username)
473
491
474 # update repo changeset caches initially
492 # update repo changeset caches initially
475 db_repo.update_changeset_cache()
493 db_repo.update_changeset_cache()
476
494
477 # set new created state
495 # set new created state
478 db_repo.set_state(db.Repository.STATE_CREATED)
496 db_repo.set_state(db.Repository.STATE_CREATED)
479 DBS.commit()
497 DBS.commit()
480 except Exception as e:
498 except Exception as e:
481 log.warning('Exception %s occurred when forking repository, '
499 log.warning('Exception %s occurred when forking repository, '
482 'doing cleanup...' % e)
500 'doing cleanup...' % e)
483 # rollback things manually !
501 # rollback things manually !
484 db_repo = db.Repository.get_by_repo_name(repo_name_full)
502 db_repo = db.Repository.get_by_repo_name(repo_name_full)
485 if db_repo:
503 if db_repo:
486 db.Repository.delete(db_repo.repo_id)
504 db.Repository.delete(db_repo.repo_id)
487 DBS.commit()
505 DBS.commit()
488 repo.RepoModel()._delete_filesystem_repo(db_repo)
506 repo.RepoModel()._delete_filesystem_repo(db_repo)
489 raise
507 raise
490
508
491 return True
509 return True
492
510
493
511
494 def __get_codes_stats(repo_name):
512 def __get_codes_stats(repo_name):
495 scm_repo = db.Repository.get_by_repo_name(repo_name).scm_instance
513 scm_repo = db.Repository.get_by_repo_name(repo_name).scm_instance
496
514
497 tip = scm_repo.get_changeset()
515 tip = scm_repo.get_changeset()
498 code_stats = {}
516 code_stats = {}
499
517
500 for _topnode, _dirnodes, filenodes in tip.walk('/'):
518 for _topnode, _dirnodes, filenodes in tip.walk('/'):
501 for filenode in filenodes:
519 for filenode in filenodes:
502 ext = filenode.extension.lower()
520 ext = filenode.extension.lower()
503 if ext in conf.LANGUAGES_EXTENSIONS_MAP and not filenode.is_binary:
521 if ext in conf.LANGUAGES_EXTENSIONS_MAP and not filenode.is_binary:
504 if ext in code_stats:
522 if ext in code_stats:
505 code_stats[ext] += 1
523 code_stats[ext] += 1
506 else:
524 else:
507 code_stats[ext] = 1
525 code_stats[ext] = 1
508
526
509 return code_stats or {}
527 return code_stats or {}
@@ -1,294 +1,293 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
2
2
3
3
4 import re
4 import re
5 import sys
5 import sys
6
6
7
7
8 ignored_modules = set('''
8 ignored_modules = set('''
9 argparse
9 argparse
10 base64
10 base64
11 bcrypt
11 bcrypt
12 binascii
12 binascii
13 bleach
13 bleach
14 calendar
14 calendar
15 celery
15 celery
16 celery
16 celery
17 chardet
17 chardet
18 click
18 click
19 collections
19 collections
20 configparser
20 configparser
21 copy
21 copy
22 csv
22 csv
23 ctypes
23 ctypes
24 datetime
24 datetime
25 dateutil
25 dateutil
26 decimal
26 decimal
27 decorator
27 decorator
28 difflib
28 difflib
29 distutils
29 distutils
30 docutils
30 docutils
31 email
31 email
32 errno
32 errno
33 fileinput
33 fileinput
34 functools
34 functools
35 getpass
35 getpass
36 grp
36 grp
37 hashlib
37 hashlib
38 hmac
38 hmac
39 html
39 html
40 http
40 http
41 imp
41 imp
42 importlib
42 importlib
43 inspect
43 inspect
44 io
44 io
45 ipaddr
45 ipaddr
46 IPython
46 IPython
47 isapi_wsgi
47 isapi_wsgi
48 itertools
48 itertools
49 json
49 json
50 kajiki
50 kajiki
51 ldap
51 ldap
52 logging
52 logging
53 mako
53 mako
54 markdown
54 markdown
55 mimetypes
55 mimetypes
56 mock
56 mock
57 msvcrt
57 msvcrt
58 multiprocessing
58 multiprocessing
59 operator
59 operator
60 os
60 os
61 paginate
61 paginate
62 paginate_sqlalchemy
62 paginate_sqlalchemy
63 pam
63 pam
64 paste
64 paste
65 pkg_resources
65 pkg_resources
66 platform
66 platform
67 posixpath
67 posixpath
68 pprint
68 pprint
69 pwd
69 pwd
70 pyflakes
70 pyflakes
71 pytest
71 pytest
72 pytest_localserver
72 pytest_localserver
73 random
73 random
74 re
74 re
75 routes
75 routes
76 setuptools
76 setuptools
77 shlex
77 shlex
78 shutil
78 shutil
79 smtplib
79 smtplib
80 socket
80 socket
81 ssl
81 ssl
82 stat
82 stat
83 string
83 string
84 struct
84 struct
85 subprocess
85 subprocess
86 sys
86 sys
87 tarfile
87 tarfile
88 tempfile
88 tempfile
89 textwrap
89 textwrap
90 tgext
90 tgext
91 threading
91 threading
92 time
92 time
93 traceback
93 traceback
94 traitlets
94 traitlets
95 types
95 types
96 urllib
96 urllib
97 urlobject
97 urlobject
98 uuid
98 uuid
99 warnings
99 warnings
100 webhelpers2
100 webhelpers2
101 webob
101 webob
102 webtest
102 webtest
103 whoosh
103 whoosh
104 win32traceutil
104 win32traceutil
105 zipfile
105 zipfile
106 '''.split())
106 '''.split())
107
107
108 top_modules = set('''
108 top_modules = set('''
109 kallithea.alembic
109 kallithea.alembic
110 kallithea.bin
110 kallithea.bin
111 kallithea.config
111 kallithea.config
112 kallithea.controllers
112 kallithea.controllers
113 kallithea.templates.py
113 kallithea.templates.py
114 scripts
114 scripts
115 '''.split())
115 '''.split())
116
116
117 bottom_external_modules = set('''
117 bottom_external_modules = set('''
118 tg
118 tg
119 mercurial
119 mercurial
120 sqlalchemy
120 sqlalchemy
121 alembic
121 alembic
122 formencode
122 formencode
123 pygments
123 pygments
124 dulwich
124 dulwich
125 beaker
125 beaker
126 psycopg2
126 psycopg2
127 docs
127 docs
128 setup
128 setup
129 conftest
129 conftest
130 '''.split())
130 '''.split())
131
131
132 normal_modules = set('''
132 normal_modules = set('''
133 kallithea
133 kallithea
134 kallithea.controllers.base
134 kallithea.controllers.base
135 kallithea.lib
135 kallithea.lib
136 kallithea.lib.auth
136 kallithea.lib.auth
137 kallithea.lib.auth_modules
137 kallithea.lib.auth_modules
138 kallithea.lib.celerylib
138 kallithea.lib.celerylib
139 kallithea.lib.db_manage
139 kallithea.lib.db_manage
140 kallithea.lib.helpers
140 kallithea.lib.helpers
141 kallithea.lib.hooks
141 kallithea.lib.hooks
142 kallithea.lib.indexers
142 kallithea.lib.indexers
143 kallithea.lib.utils
143 kallithea.lib.utils
144 kallithea.lib.utils2
144 kallithea.lib.utils2
145 kallithea.lib.vcs
145 kallithea.lib.vcs
146 kallithea.lib.webutils
146 kallithea.lib.webutils
147 kallithea.model
147 kallithea.model
148 kallithea.model.async_tasks
148 kallithea.model.async_tasks
149 kallithea.model.scm
149 kallithea.model.scm
150 kallithea.templates.py
150 kallithea.templates.py
151 '''.split())
151 '''.split())
152
152
153 shown_modules = normal_modules | top_modules
153 shown_modules = normal_modules | top_modules
154
154
155 # break the chains somehow - this is a cleanup TODO list
155 # break the chains somehow - this is a cleanup TODO list
156 known_violations = [
156 known_violations = [
157 ('kallithea.lib.auth_modules', 'kallithea.lib.auth'), # needs base&facade
157 ('kallithea.lib.auth_modules', 'kallithea.lib.auth'), # needs base&facade
158 ('kallithea.lib.utils', 'kallithea.model'), # clean up utils
158 ('kallithea.lib.utils', 'kallithea.model'), # clean up utils
159 ('kallithea.lib.utils', 'kallithea.model.db'),
159 ('kallithea.lib.utils', 'kallithea.model.db'),
160 ('kallithea.lib.utils', 'kallithea.model.scm'),
160 ('kallithea.lib.utils', 'kallithea.model.scm'),
161 ('kallithea.model.async_tasks', 'kallithea.lib.helpers'),
162 ('kallithea.model.async_tasks', 'kallithea.lib.hooks'),
161 ('kallithea.model.async_tasks', 'kallithea.lib.hooks'),
163 ('kallithea.model.async_tasks', 'kallithea.lib.indexers'),
162 ('kallithea.model.async_tasks', 'kallithea.lib.indexers'),
164 ('kallithea.model.async_tasks', 'kallithea.model'),
163 ('kallithea.model.async_tasks', 'kallithea.model'),
165 ('kallithea.model', 'kallithea.lib.auth'), # auth.HasXXX
164 ('kallithea.model', 'kallithea.lib.auth'), # auth.HasXXX
166 ('kallithea.model', 'kallithea.lib.auth_modules'), # validators
165 ('kallithea.model', 'kallithea.lib.auth_modules'), # validators
167 ('kallithea.model', 'kallithea.lib.hooks'), # clean up hooks
166 ('kallithea.model', 'kallithea.lib.hooks'), # clean up hooks
168 ('kallithea.model', 'kallithea.model.scm'),
167 ('kallithea.model', 'kallithea.model.scm'),
169 ('kallithea.model.scm', 'kallithea.lib.hooks'),
168 ('kallithea.model.scm', 'kallithea.lib.hooks'),
170 ]
169 ]
171
170
172 extra_edges = [
171 extra_edges = [
173 ('kallithea.config', 'kallithea.controllers'), # through TG
172 ('kallithea.config', 'kallithea.controllers'), # through TG
174 ('kallithea.lib.auth', 'kallithea.lib.auth_modules'), # custom loader
173 ('kallithea.lib.auth', 'kallithea.lib.auth_modules'), # custom loader
175 ]
174 ]
176
175
177
176
178 def normalize(s):
177 def normalize(s):
179 """Given a string with dot path, return the string it should be shown as."""
178 """Given a string with dot path, return the string it should be shown as."""
180 parts = s.replace('.__init__', '').split('.')
179 parts = s.replace('.__init__', '').split('.')
181 short_2 = '.'.join(parts[:2])
180 short_2 = '.'.join(parts[:2])
182 short_3 = '.'.join(parts[:3])
181 short_3 = '.'.join(parts[:3])
183 short_4 = '.'.join(parts[:4])
182 short_4 = '.'.join(parts[:4])
184 if parts[0] in ['scripts', 'contributor_data', 'i18n_utils']:
183 if parts[0] in ['scripts', 'contributor_data', 'i18n_utils']:
185 return 'scripts'
184 return 'scripts'
186 if short_3 == 'kallithea.model.meta':
185 if short_3 == 'kallithea.model.meta':
187 return 'kallithea.model.db'
186 return 'kallithea.model.db'
188 if parts[:4] == ['kallithea', 'lib', 'vcs', 'ssh']:
187 if parts[:4] == ['kallithea', 'lib', 'vcs', 'ssh']:
189 return 'kallithea.lib.vcs.ssh'
188 return 'kallithea.lib.vcs.ssh'
190 if short_4 in shown_modules:
189 if short_4 in shown_modules:
191 return short_4
190 return short_4
192 if short_3 in shown_modules:
191 if short_3 in shown_modules:
193 return short_3
192 return short_3
194 if short_2 in shown_modules:
193 if short_2 in shown_modules:
195 return short_2
194 return short_2
196 if short_2 == 'kallithea.tests':
195 if short_2 == 'kallithea.tests':
197 return None
196 return None
198 if parts[0] in ignored_modules:
197 if parts[0] in ignored_modules:
199 return None
198 return None
200 assert parts[0] in bottom_external_modules, parts
199 assert parts[0] in bottom_external_modules, parts
201 return parts[0]
200 return parts[0]
202
201
203
202
204 def main(filenames):
203 def main(filenames):
205 if not filenames or filenames[0].startswith('-'):
204 if not filenames or filenames[0].startswith('-'):
206 print('''\
205 print('''\
207 Usage:
206 Usage:
208 hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/deps.py
207 hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/deps.py
209 dot -Tsvg deps.dot > deps.svg
208 dot -Tsvg deps.dot > deps.svg
210 ''')
209 ''')
211 raise SystemExit(1)
210 raise SystemExit(1)
212
211
213 files_imports = dict() # map filenames to its imports
212 files_imports = dict() # map filenames to its imports
214 import_deps = set() # set of tuples with module name and its imports
213 import_deps = set() # set of tuples with module name and its imports
215 for fn in filenames:
214 for fn in filenames:
216 with open(fn) as f:
215 with open(fn) as f:
217 s = f.read()
216 s = f.read()
218
217
219 dot_name = (fn[:-3] if fn.endswith('.py') else fn).replace('/', '.')
218 dot_name = (fn[:-3] if fn.endswith('.py') else fn).replace('/', '.')
220 file_imports = set()
219 file_imports = set()
221 for m in re.finditer(r'^ *(?:from ([^ ]*) import (?:([a-zA-Z].*)|\(([^)]*)\))|import (.*))$', s, re.MULTILINE):
220 for m in re.finditer(r'^ *(?:from ([^ ]*) import (?:([a-zA-Z].*)|\(([^)]*)\))|import (.*))$', s, re.MULTILINE):
222 m_from, m_from_import, m_from_import2, m_import = m.groups()
221 m_from, m_from_import, m_from_import2, m_import = m.groups()
223 if m_from:
222 if m_from:
224 pre = m_from + '.'
223 pre = m_from + '.'
225 if pre.startswith('.'):
224 if pre.startswith('.'):
226 pre = dot_name.rsplit('.', 1)[0] + pre
225 pre = dot_name.rsplit('.', 1)[0] + pre
227 importlist = m_from_import or m_from_import2
226 importlist = m_from_import or m_from_import2
228 else:
227 else:
229 pre = ''
228 pre = ''
230 importlist = m_import
229 importlist = m_import
231 for imp in importlist.split('#', 1)[0].split(','):
230 for imp in importlist.split('#', 1)[0].split(','):
232 full_imp = pre + imp.strip().split(' as ', 1)[0]
231 full_imp = pre + imp.strip().split(' as ', 1)[0]
233 file_imports.add(full_imp)
232 file_imports.add(full_imp)
234 import_deps.add((dot_name, full_imp))
233 import_deps.add((dot_name, full_imp))
235 files_imports[fn] = file_imports
234 files_imports[fn] = file_imports
236
235
237 # dump out all deps for debugging and analysis
236 # dump out all deps for debugging and analysis
238 with open('deps.txt', 'w') as f:
237 with open('deps.txt', 'w') as f:
239 for fn, file_imports in sorted(files_imports.items()):
238 for fn, file_imports in sorted(files_imports.items()):
240 for file_import in sorted(file_imports):
239 for file_import in sorted(file_imports):
241 if file_import.split('.', 1)[0] in ignored_modules:
240 if file_import.split('.', 1)[0] in ignored_modules:
242 continue
241 continue
243 f.write('%s: %s\n' % (fn, file_import))
242 f.write('%s: %s\n' % (fn, file_import))
244
243
245 # find leafs that haven't been ignored - they are the important external dependencies and shown in the bottom row
244 # find leafs that haven't been ignored - they are the important external dependencies and shown in the bottom row
246 only_imported = set(
245 only_imported = set(
247 set(normalize(b) for a, b in import_deps) -
246 set(normalize(b) for a, b in import_deps) -
248 set(normalize(a) for a, b in import_deps) -
247 set(normalize(a) for a, b in import_deps) -
249 set([None, 'kallithea'])
248 set([None, 'kallithea'])
250 )
249 )
251
250
252 normalized_dep_edges = set()
251 normalized_dep_edges = set()
253 for dot_name, full_imp in import_deps:
252 for dot_name, full_imp in import_deps:
254 a = normalize(dot_name)
253 a = normalize(dot_name)
255 b = normalize(full_imp)
254 b = normalize(full_imp)
256 if a is None or b is None or a == b:
255 if a is None or b is None or a == b:
257 continue
256 continue
258 normalized_dep_edges.add((a, b))
257 normalized_dep_edges.add((a, b))
259 #print((dot_name, full_imp, a, b))
258 #print((dot_name, full_imp, a, b))
260 normalized_dep_edges.update(extra_edges)
259 normalized_dep_edges.update(extra_edges)
261
260
262 unseen_shown_modules = shown_modules.difference(a for a, b in normalized_dep_edges).difference(b for a, b in normalized_dep_edges)
261 unseen_shown_modules = shown_modules.difference(a for a, b in normalized_dep_edges).difference(b for a, b in normalized_dep_edges)
263 assert not unseen_shown_modules, unseen_shown_modules
262 assert not unseen_shown_modules, unseen_shown_modules
264
263
265 with open('deps.dot', 'w') as f:
264 with open('deps.dot', 'w') as f:
266 f.write('digraph {\n')
265 f.write('digraph {\n')
267 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(top_modules)))
266 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(top_modules)))
268 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(only_imported)))
267 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(only_imported)))
269 for a, b in sorted(normalized_dep_edges):
268 for a, b in sorted(normalized_dep_edges):
270 f.write(' "%s" -> "%s"%s\n' % (a, b, ' [color=red]' if (a, b) in known_violations else ' [color=green]' if (a, b) in extra_edges else ''))
269 f.write(' "%s" -> "%s"%s\n' % (a, b, ' [color=red]' if (a, b) in known_violations else ' [color=green]' if (a, b) in extra_edges else ''))
271 f.write('}\n')
270 f.write('}\n')
272
271
273 # verify dependencies by untangling dependency chain bottom-up:
272 # verify dependencies by untangling dependency chain bottom-up:
274 todo = set(normalized_dep_edges)
273 todo = set(normalized_dep_edges)
275 for x in known_violations:
274 for x in known_violations:
276 todo.remove(x)
275 todo.remove(x)
277
276
278 while todo:
277 while todo:
279 depending = set(a for a, b in todo)
278 depending = set(a for a, b in todo)
280 depended = set(b for a, b in todo)
279 depended = set(b for a, b in todo)
281 drop = depended - depending
280 drop = depended - depending
282 if not drop:
281 if not drop:
283 print('ERROR: cycles:', len(todo))
282 print('ERROR: cycles:', len(todo))
284 for x in sorted(todo):
283 for x in sorted(todo):
285 print('%s,' % (x,))
284 print('%s,' % (x,))
286 raise SystemExit(1)
285 raise SystemExit(1)
287 #for do_b in sorted(drop):
286 #for do_b in sorted(drop):
288 # print('Picking', do_b, '- unblocks:', ' '.join(a for a, b in sorted((todo)) if b == do_b))
287 # print('Picking', do_b, '- unblocks:', ' '.join(a for a, b in sorted((todo)) if b == do_b))
289 todo = set((a, b) for a, b in todo if b in depending)
288 todo = set((a, b) for a, b in todo if b in depending)
290 #print()
289 #print()
291
290
292
291
293 if __name__ == '__main__':
292 if __name__ == '__main__':
294 main(sys.argv[1:])
293 main(sys.argv[1:])
General Comments 0
You need to be logged in to leave comments. Login now