##// END OF EJS Templates
Improved RSS/ATOM feeds...
marcink -
r2385:a455b2c7 beta
parent child Browse files
Show More
@@ -1,706 +1,715 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.api
3 rhodecode.controllers.api
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 API controller for RhodeCode
6 API controller for RhodeCode
7
7
8 :created_on: Aug 20, 2011
8 :created_on: Aug 20, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
27
27
28 import traceback
28 import traceback
29 import logging
29 import logging
30
30
31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
33 HasPermissionAnyDecorator, PasswordGenerator, AuthUser
33 HasPermissionAnyDecorator, PasswordGenerator, AuthUser
34
34
35 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
36 from rhodecode.model.scm import ScmModel
36 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.db import User, UsersGroup, Repository
37 from rhodecode.model.db import User, UsersGroup, Repository
38 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.user import UserModel
39 from rhodecode.model.user import UserModel
40 from rhodecode.model.users_group import UsersGroupModel
40 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.lib.utils import map_groups
41 from rhodecode.lib.utils import map_groups
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class ApiController(JSONRPCController):
46 class ApiController(JSONRPCController):
47 """
47 """
48 API Controller
48 API Controller
49
49
50
50
51 Each method needs to have USER as argument this is then based on given
51 Each method needs to have USER as argument this is then based on given
52 API_KEY propagated as instance of user object
52 API_KEY propagated as instance of user object
53
53
54 Preferably this should be first argument also
54 Preferably this should be first argument also
55
55
56
56
57 Each function should also **raise** JSONRPCError for any
57 Each function should also **raise** JSONRPCError for any
58 errors that happens
58 errors that happens
59
59
60 """
60 """
61
61
62 @HasPermissionAllDecorator('hg.admin')
62 @HasPermissionAllDecorator('hg.admin')
63 def pull(self, apiuser, repo_name):
63 def pull(self, apiuser, repo_name):
64 """
64 """
65 Dispatch pull action on given repo
65 Dispatch pull action on given repo
66
66
67
67
68 :param user:
68 :param user:
69 :param repo_name:
69 :param repo_name:
70 """
70 """
71
71
72 if Repository.is_valid(repo_name) is False:
72 if Repository.is_valid(repo_name) is False:
73 raise JSONRPCError('Unknown repo "%s"' % repo_name)
73 raise JSONRPCError('Unknown repo "%s"' % repo_name)
74
74
75 try:
75 try:
76 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
76 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
77 return 'Pulled from %s' % repo_name
77 return 'Pulled from %s' % repo_name
78 except Exception:
78 except Exception:
79 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
79 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
80
80
81 @HasPermissionAllDecorator('hg.admin')
81 @HasPermissionAllDecorator('hg.admin')
82 def get_user(self, apiuser, userid):
82 def get_user(self, apiuser, userid):
83 """"
83 """"
84 Get a user by username
84 Get a user by username
85
85
86 :param apiuser:
86 :param apiuser:
87 :param username:
87 :param username:
88 """
88 """
89
89
90 user = UserModel().get_user(userid)
90 user = UserModel().get_user(userid)
91 if user is None:
91 if user is None:
92 return user
92 return user
93
93
94 return dict(
94 return dict(
95 id=user.user_id,
95 id=user.user_id,
96 username=user.username,
96 username=user.username,
97 firstname=user.name,
97 firstname=user.name,
98 lastname=user.lastname,
98 lastname=user.lastname,
99 email=user.email,
99 email=user.email,
100 active=user.active,
100 active=user.active,
101 admin=user.admin,
101 admin=user.admin,
102 ldap_dn=user.ldap_dn,
102 ldap_dn=user.ldap_dn,
103 last_login=user.last_login,
103 last_login=user.last_login,
104 permissions=AuthUser(user_id=user.user_id).permissions
104 permissions=AuthUser(user_id=user.user_id).permissions
105 )
105 )
106
106
107 @HasPermissionAllDecorator('hg.admin')
107 @HasPermissionAllDecorator('hg.admin')
108 def get_users(self, apiuser):
108 def get_users(self, apiuser):
109 """"
109 """"
110 Get all users
110 Get all users
111
111
112 :param apiuser:
112 :param apiuser:
113 """
113 """
114
114
115 result = []
115 result = []
116 for user in User.getAll():
116 for user in User.getAll():
117 result.append(
117 result.append(
118 dict(
118 dict(
119 id=user.user_id,
119 id=user.user_id,
120 username=user.username,
120 username=user.username,
121 firstname=user.name,
121 firstname=user.name,
122 lastname=user.lastname,
122 lastname=user.lastname,
123 email=user.email,
123 email=user.email,
124 active=user.active,
124 active=user.active,
125 admin=user.admin,
125 admin=user.admin,
126 ldap_dn=user.ldap_dn,
126 ldap_dn=user.ldap_dn,
127 last_login=user.last_login,
127 last_login=user.last_login,
128 )
128 )
129 )
129 )
130 return result
130 return result
131
131
132 @HasPermissionAllDecorator('hg.admin')
132 @HasPermissionAllDecorator('hg.admin')
133 def create_user(self, apiuser, username, email, password, firstname=None,
133 def create_user(self, apiuser, username, email, password, firstname=None,
134 lastname=None, active=True, admin=False, ldap_dn=None):
134 lastname=None, active=True, admin=False, ldap_dn=None):
135 """
135 """
136 Create new user
136 Create new user
137
137
138 :param apiuser:
138 :param apiuser:
139 :param username:
139 :param username:
140 :param password:
140 :param password:
141 :param email:
141 :param email:
142 :param name:
142 :param name:
143 :param lastname:
143 :param lastname:
144 :param active:
144 :param active:
145 :param admin:
145 :param admin:
146 :param ldap_dn:
146 :param ldap_dn:
147 """
147 """
148 if User.get_by_username(username):
148 if User.get_by_username(username):
149 raise JSONRPCError("user %s already exist" % username)
149 raise JSONRPCError("user %s already exist" % username)
150
150
151 if User.get_by_email(email, case_insensitive=True):
151 if User.get_by_email(email, case_insensitive=True):
152 raise JSONRPCError("email %s already exist" % email)
152 raise JSONRPCError("email %s already exist" % email)
153
153
154 if ldap_dn:
154 if ldap_dn:
155 # generate temporary password if ldap_dn
155 # generate temporary password if ldap_dn
156 password = PasswordGenerator().gen_password(length=8)
156 password = PasswordGenerator().gen_password(length=8)
157
157
158 try:
158 try:
159 user = UserModel().create_or_update(
159 user = UserModel().create_or_update(
160 username, password, email, firstname,
160 username, password, email, firstname,
161 lastname, active, admin, ldap_dn
161 lastname, active, admin, ldap_dn
162 )
162 )
163 Session.commit()
163 Session.commit()
164 return dict(
164 return dict(
165 id=user.user_id,
165 id=user.user_id,
166 msg='created new user %s' % username,
166 msg='created new user %s' % username,
167 user=dict(
167 user=dict(
168 id=user.user_id,
168 id=user.user_id,
169 username=user.username,
169 username=user.username,
170 firstname=user.name,
170 firstname=user.name,
171 lastname=user.lastname,
171 lastname=user.lastname,
172 email=user.email,
172 email=user.email,
173 active=user.active,
173 active=user.active,
174 admin=user.admin,
174 admin=user.admin,
175 ldap_dn=user.ldap_dn,
175 ldap_dn=user.ldap_dn,
176 last_login=user.last_login,
176 last_login=user.last_login,
177 )
177 )
178 )
178 )
179 except Exception:
179 except Exception:
180 log.error(traceback.format_exc())
180 log.error(traceback.format_exc())
181 raise JSONRPCError('failed to create user %s' % username)
181 raise JSONRPCError('failed to create user %s' % username)
182
182
183 @HasPermissionAllDecorator('hg.admin')
183 @HasPermissionAllDecorator('hg.admin')
184 def update_user(self, apiuser, userid, username, password, email,
184 def update_user(self, apiuser, userid, username, password, email,
185 firstname, lastname, active, admin, ldap_dn):
185 firstname, lastname, active, admin, ldap_dn):
186 """
186 """
187 Updates given user
187 Updates given user
188
188
189 :param apiuser:
189 :param apiuser:
190 :param username:
190 :param username:
191 :param password:
191 :param password:
192 :param email:
192 :param email:
193 :param name:
193 :param name:
194 :param lastname:
194 :param lastname:
195 :param active:
195 :param active:
196 :param admin:
196 :param admin:
197 :param ldap_dn:
197 :param ldap_dn:
198 """
198 """
199 usr = UserModel().get_user(userid)
199 usr = UserModel().get_user(userid)
200 if not usr:
200 if not usr:
201 raise JSONRPCError("user ID:%s does not exist" % userid)
201 raise JSONRPCError("user ID:%s does not exist" % userid)
202
202
203 try:
203 try:
204 usr = UserModel().create_or_update(
204 usr = UserModel().create_or_update(
205 username, password, email, firstname,
205 username, password, email, firstname,
206 lastname, active, admin, ldap_dn
206 lastname, active, admin, ldap_dn
207 )
207 )
208 Session.commit()
208 Session.commit()
209 return dict(
209 return dict(
210 id=usr.user_id,
210 id=usr.user_id,
211 msg='updated user ID:%s %s' % (usr.user_id, usr.username)
211 msg='updated user ID:%s %s' % (usr.user_id, usr.username)
212 )
212 )
213 except Exception:
213 except Exception:
214 log.error(traceback.format_exc())
214 log.error(traceback.format_exc())
215 raise JSONRPCError('failed to update user %s' % userid)
215 raise JSONRPCError('failed to update user %s' % userid)
216
216
217 @HasPermissionAllDecorator('hg.admin')
217 @HasPermissionAllDecorator('hg.admin')
218 def delete_user(self, apiuser, userid):
218 def delete_user(self, apiuser, userid):
219 """"
219 """"
220 Deletes an user
220 Deletes an user
221
221
222 :param apiuser:
222 :param apiuser:
223 """
223 """
224 usr = UserModel().get_user(userid)
224 usr = UserModel().get_user(userid)
225 if not usr:
225 if not usr:
226 raise JSONRPCError("user ID:%s does not exist" % userid)
226 raise JSONRPCError("user ID:%s does not exist" % userid)
227
227
228 try:
228 try:
229 UserModel().delete(userid)
229 UserModel().delete(userid)
230 Session.commit()
230 Session.commit()
231 return dict(
231 return dict(
232 id=usr.user_id,
232 id=usr.user_id,
233 msg='deleted user ID:%s %s' % (usr.user_id, usr.username)
233 msg='deleted user ID:%s %s' % (usr.user_id, usr.username)
234 )
234 )
235 except Exception:
235 except Exception:
236 log.error(traceback.format_exc())
236 log.error(traceback.format_exc())
237 raise JSONRPCError('failed to delete ID:%s %s' % (usr.user_id,
237 raise JSONRPCError('failed to delete ID:%s %s' % (usr.user_id,
238 usr.username))
238 usr.username))
239
239
240 @HasPermissionAllDecorator('hg.admin')
240 @HasPermissionAllDecorator('hg.admin')
241 def get_users_group(self, apiuser, group_name):
241 def get_users_group(self, apiuser, group_name):
242 """"
242 """"
243 Get users group by name
243 Get users group by name
244
244
245 :param apiuser:
245 :param apiuser:
246 :param group_name:
246 :param group_name:
247 """
247 """
248
248
249 users_group = UsersGroup.get_by_group_name(group_name)
249 users_group = UsersGroup.get_by_group_name(group_name)
250 if not users_group:
250 if not users_group:
251 return None
251 return None
252
252
253 members = []
253 members = []
254 for user in users_group.members:
254 for user in users_group.members:
255 user = user.user
255 user = user.user
256 members.append(dict(id=user.user_id,
256 members.append(dict(id=user.user_id,
257 username=user.username,
257 username=user.username,
258 firstname=user.name,
258 firstname=user.name,
259 lastname=user.lastname,
259 lastname=user.lastname,
260 email=user.email,
260 email=user.email,
261 active=user.active,
261 active=user.active,
262 admin=user.admin,
262 admin=user.admin,
263 ldap=user.ldap_dn))
263 ldap=user.ldap_dn))
264
264
265 return dict(id=users_group.users_group_id,
265 return dict(id=users_group.users_group_id,
266 group_name=users_group.users_group_name,
266 group_name=users_group.users_group_name,
267 active=users_group.users_group_active,
267 active=users_group.users_group_active,
268 members=members)
268 members=members)
269
269
270 @HasPermissionAllDecorator('hg.admin')
270 @HasPermissionAllDecorator('hg.admin')
271 def get_users_groups(self, apiuser):
271 def get_users_groups(self, apiuser):
272 """"
272 """"
273 Get all users groups
273 Get all users groups
274
274
275 :param apiuser:
275 :param apiuser:
276 """
276 """
277
277
278 result = []
278 result = []
279 for users_group in UsersGroup.getAll():
279 for users_group in UsersGroup.getAll():
280 members = []
280 members = []
281 for user in users_group.members:
281 for user in users_group.members:
282 user = user.user
282 user = user.user
283 members.append(dict(id=user.user_id,
283 members.append(dict(id=user.user_id,
284 username=user.username,
284 username=user.username,
285 firstname=user.name,
285 firstname=user.name,
286 lastname=user.lastname,
286 lastname=user.lastname,
287 email=user.email,
287 email=user.email,
288 active=user.active,
288 active=user.active,
289 admin=user.admin,
289 admin=user.admin,
290 ldap=user.ldap_dn))
290 ldap=user.ldap_dn))
291
291
292 result.append(dict(id=users_group.users_group_id,
292 result.append(dict(id=users_group.users_group_id,
293 group_name=users_group.users_group_name,
293 group_name=users_group.users_group_name,
294 active=users_group.users_group_active,
294 active=users_group.users_group_active,
295 members=members))
295 members=members))
296 return result
296 return result
297
297
298 @HasPermissionAllDecorator('hg.admin')
298 @HasPermissionAllDecorator('hg.admin')
299 def create_users_group(self, apiuser, group_name, active=True):
299 def create_users_group(self, apiuser, group_name, active=True):
300 """
300 """
301 Creates an new usergroup
301 Creates an new usergroup
302
302
303 :param group_name:
303 :param group_name:
304 :param active:
304 :param active:
305 """
305 """
306
306
307 if self.get_users_group(apiuser, group_name):
307 if self.get_users_group(apiuser, group_name):
308 raise JSONRPCError("users group %s already exist" % group_name)
308 raise JSONRPCError("users group %s already exist" % group_name)
309
309
310 try:
310 try:
311 ug = UsersGroupModel().create(name=group_name, active=active)
311 ug = UsersGroupModel().create(name=group_name, active=active)
312 Session.commit()
312 Session.commit()
313 return dict(id=ug.users_group_id,
313 return dict(id=ug.users_group_id,
314 msg='created new users group %s' % group_name)
314 msg='created new users group %s' % group_name)
315 except Exception:
315 except Exception:
316 log.error(traceback.format_exc())
316 log.error(traceback.format_exc())
317 raise JSONRPCError('failed to create group %s' % group_name)
317 raise JSONRPCError('failed to create group %s' % group_name)
318
318
319 @HasPermissionAllDecorator('hg.admin')
319 @HasPermissionAllDecorator('hg.admin')
320 def add_user_to_users_group(self, apiuser, group_name, username):
320 def add_user_to_users_group(self, apiuser, group_name, username):
321 """"
321 """"
322 Add a user to a users group
322 Add a user to a users group
323
323
324 :param apiuser:
324 :param apiuser:
325 :param group_name:
325 :param group_name:
326 :param username:
326 :param username:
327 """
327 """
328
328
329 try:
329 try:
330 users_group = UsersGroup.get_by_group_name(group_name)
330 users_group = UsersGroup.get_by_group_name(group_name)
331 if not users_group:
331 if not users_group:
332 raise JSONRPCError('unknown users group %s' % group_name)
332 raise JSONRPCError('unknown users group %s' % group_name)
333
333
334 user = User.get_by_username(username)
334 user = User.get_by_username(username)
335 if user is None:
335 if user is None:
336 raise JSONRPCError('unknown user %s' % username)
336 raise JSONRPCError('unknown user %s' % username)
337
337
338 ugm = UsersGroupModel().add_user_to_group(users_group, user)
338 ugm = UsersGroupModel().add_user_to_group(users_group, user)
339 success = True if ugm != True else False
339 success = True if ugm != True else False
340 msg = 'added member %s to users group %s' % (username, group_name)
340 msg = 'added member %s to users group %s' % (username, group_name)
341 msg = msg if success else 'User is already in that group'
341 msg = msg if success else 'User is already in that group'
342 Session.commit()
342 Session.commit()
343
343
344 return dict(
344 return dict(
345 id=ugm.users_group_member_id if ugm != True else None,
345 id=ugm.users_group_member_id if ugm != True else None,
346 success=success,
346 success=success,
347 msg=msg
347 msg=msg
348 )
348 )
349 except Exception:
349 except Exception:
350 log.error(traceback.format_exc())
350 log.error(traceback.format_exc())
351 raise JSONRPCError('failed to add users group member')
351 raise JSONRPCError('failed to add users group member')
352
352
353 @HasPermissionAllDecorator('hg.admin')
353 @HasPermissionAllDecorator('hg.admin')
354 def remove_user_from_users_group(self, apiuser, group_name, username):
354 def remove_user_from_users_group(self, apiuser, group_name, username):
355 """
355 """
356 Remove user from a group
356 Remove user from a group
357
357
358 :param apiuser
358 :param apiuser
359 :param group_name
359 :param group_name
360 :param username
360 :param username
361 """
361 """
362
362
363 try:
363 try:
364 users_group = UsersGroup.get_by_group_name(group_name)
364 users_group = UsersGroup.get_by_group_name(group_name)
365 if not users_group:
365 if not users_group:
366 raise JSONRPCError('unknown users group %s' % group_name)
366 raise JSONRPCError('unknown users group %s' % group_name)
367
367
368 user = User.get_by_username(username)
368 user = User.get_by_username(username)
369 if user is None:
369 if user is None:
370 raise JSONRPCError('unknown user %s' % username)
370 raise JSONRPCError('unknown user %s' % username)
371
371
372 success = UsersGroupModel().remove_user_from_group(users_group, user)
372 success = UsersGroupModel().remove_user_from_group(users_group, user)
373 msg = 'removed member %s from users group %s' % (username, group_name)
373 msg = 'removed member %s from users group %s' % (username, group_name)
374 msg = msg if success else "User wasn't in group"
374 msg = msg if success else "User wasn't in group"
375 Session.commit()
375 Session.commit()
376 return dict(success=success, msg=msg)
376 return dict(success=success, msg=msg)
377 except Exception:
377 except Exception:
378 log.error(traceback.format_exc())
378 log.error(traceback.format_exc())
379 raise JSONRPCError('failed to remove user from group')
379 raise JSONRPCError('failed to remove user from group')
380
380
381 @HasPermissionAnyDecorator('hg.admin')
381 @HasPermissionAnyDecorator('hg.admin')
382 def get_repo(self, apiuser, repoid):
382 def get_repo(self, apiuser, repoid):
383 """"
383 """"
384 Get repository by name
384 Get repository by name
385
385
386 :param apiuser:
386 :param apiuser:
387 :param repo_name:
387 :param repo_name:
388 """
388 """
389
389
390 repo = RepoModel().get_repo(repoid)
390 repo = RepoModel().get_repo(repoid)
391 if repo is None:
391 if repo is None:
392 raise JSONRPCError('unknown repository "%s"' % (repo or repoid))
392 raise JSONRPCError('unknown repository "%s"' % (repo or repoid))
393
393
394 members = []
394 members = []
395 for user in repo.repo_to_perm:
395 for user in repo.repo_to_perm:
396 perm = user.permission.permission_name
396 perm = user.permission.permission_name
397 user = user.user
397 user = user.user
398 members.append(
398 members.append(
399 dict(
399 dict(
400 type="user",
400 type="user",
401 id=user.user_id,
401 id=user.user_id,
402 username=user.username,
402 username=user.username,
403 firstname=user.name,
403 firstname=user.name,
404 lastname=user.lastname,
404 lastname=user.lastname,
405 email=user.email,
405 email=user.email,
406 active=user.active,
406 active=user.active,
407 admin=user.admin,
407 admin=user.admin,
408 ldap=user.ldap_dn,
408 ldap=user.ldap_dn,
409 permission=perm
409 permission=perm
410 )
410 )
411 )
411 )
412 for users_group in repo.users_group_to_perm:
412 for users_group in repo.users_group_to_perm:
413 perm = users_group.permission.permission_name
413 perm = users_group.permission.permission_name
414 users_group = users_group.users_group
414 users_group = users_group.users_group
415 members.append(
415 members.append(
416 dict(
416 dict(
417 type="users_group",
417 type="users_group",
418 id=users_group.users_group_id,
418 id=users_group.users_group_id,
419 name=users_group.users_group_name,
419 name=users_group.users_group_name,
420 active=users_group.users_group_active,
420 active=users_group.users_group_active,
421 permission=perm
421 permission=perm
422 )
422 )
423 )
423 )
424
424
425 return dict(
425 return dict(
426 id=repo.repo_id,
426 id=repo.repo_id,
427 repo_name=repo.repo_name,
427 repo_name=repo.repo_name,
428 type=repo.repo_type,
428 type=repo.repo_type,
429 clone_uri=repo.clone_uri,
429 clone_uri=repo.clone_uri,
430 private=repo.private,
430 private=repo.private,
431 created_on=repo.created_on,
431 created_on=repo.created_on,
432 description=repo.description,
432 description=repo.description,
433 members=members
433 members=members
434 )
434 )
435
435
436 @HasPermissionAnyDecorator('hg.admin')
436 @HasPermissionAnyDecorator('hg.admin')
437 def get_repos(self, apiuser):
437 def get_repos(self, apiuser):
438 """"
438 """"
439 Get all repositories
439 Get all repositories
440
440
441 :param apiuser:
441 :param apiuser:
442 """
442 """
443
443
444 result = []
444 result = []
445 for repo in Repository.getAll():
445 for repo in Repository.getAll():
446 result.append(
446 result.append(
447 dict(
447 dict(
448 id=repo.repo_id,
448 id=repo.repo_id,
449 repo_name=repo.repo_name,
449 repo_name=repo.repo_name,
450 type=repo.repo_type,
450 type=repo.repo_type,
451 clone_uri=repo.clone_uri,
451 clone_uri=repo.clone_uri,
452 private=repo.private,
452 private=repo.private,
453 created_on=repo.created_on,
453 created_on=repo.created_on,
454 description=repo.description,
454 description=repo.description,
455 )
455 )
456 )
456 )
457 return result
457 return result
458
458
459 @HasPermissionAnyDecorator('hg.admin')
459 @HasPermissionAnyDecorator('hg.admin')
460 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
460 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
461 ret_type='all'):
461 ret_type='all'):
462 """
462 """
463 returns a list of nodes and it's children
463 returns a list of nodes and it's children
464 for a given path at given revision. It's possible to specify ret_type
464 for a given path at given revision. It's possible to specify ret_type
465 to show only files or dirs
465 to show only files or dirs
466
466
467 :param apiuser:
467 :param apiuser:
468 :param repo_name: name of repository
468 :param repo_name: name of repository
469 :param revision: revision for which listing should be done
469 :param revision: revision for which listing should be done
470 :param root_path: path from which start displaying
470 :param root_path: path from which start displaying
471 :param ret_type: return type 'all|files|dirs' nodes
471 :param ret_type: return type 'all|files|dirs' nodes
472 """
472 """
473 try:
473 try:
474 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
474 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
475 flat=False)
475 flat=False)
476 _map = {
476 _map = {
477 'all': _d + _f,
477 'all': _d + _f,
478 'files': _f,
478 'files': _f,
479 'dirs': _d,
479 'dirs': _d,
480 }
480 }
481 return _map[ret_type]
481 return _map[ret_type]
482 except KeyError:
482 except KeyError:
483 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
483 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
484 except Exception, e:
484 except Exception, e:
485 raise JSONRPCError(e)
485 raise JSONRPCError(e)
486
486
487 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
487 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
488 def create_repo(self, apiuser, repo_name, owner_name, description='',
488 def create_repo(self, apiuser, repo_name, owner_name, description='',
489 repo_type='hg', private=False, clone_uri=None):
489 repo_type='hg', private=False, clone_uri=None):
490 """
490 """
491 Create repository, if clone_url is given it makes a remote clone
491 Create repository, if clone_url is given it makes a remote clone
492
492
493 :param apiuser:
493 :param apiuser:
494 :param repo_name:
494 :param repo_name:
495 :param owner_name:
495 :param owner_name:
496 :param description:
496 :param description:
497 :param repo_type:
497 :param repo_type:
498 :param private:
498 :param private:
499 :param clone_uri:
499 :param clone_uri:
500 """
500 """
501
501
502 try:
502 try:
503 owner = User.get_by_username(owner_name)
503 owner = User.get_by_username(owner_name)
504 if owner is None:
504 if owner is None:
505 raise JSONRPCError('unknown user %s' % owner_name)
505 raise JSONRPCError('unknown user %s' % owner_name)
506
506
507 if Repository.get_by_repo_name(repo_name):
507 if Repository.get_by_repo_name(repo_name):
508 raise JSONRPCError("repo %s already exist" % repo_name)
508 raise JSONRPCError("repo %s already exist" % repo_name)
509
509
510 groups = repo_name.split(Repository.url_sep())
510 groups = repo_name.split(Repository.url_sep())
511 real_name = groups[-1]
511 real_name = groups[-1]
512 # create structure of groups
512 # create structure of groups
513 group = map_groups(repo_name)
513 group = map_groups(repo_name)
514
514
515 repo = RepoModel().create(
515 repo = RepoModel().create(
516 dict(
516 dict(
517 repo_name=real_name,
517 repo_name=real_name,
518 repo_name_full=repo_name,
518 repo_name_full=repo_name,
519 description=description,
519 description=description,
520 private=private,
520 private=private,
521 repo_type=repo_type,
521 repo_type=repo_type,
522 repo_group=group.group_id if group else None,
522 repo_group=group.group_id if group else None,
523 clone_uri=clone_uri
523 clone_uri=clone_uri
524 )
524 )
525 )
525 )
526 Session.commit()
526 Session.commit()
527
527
528 return dict(
528 return dict(
529 id=repo.repo_id,
529 id=repo.repo_id,
530 msg="Created new repository %s" % (repo.repo_name),
530 msg="Created new repository %s" % (repo.repo_name),
531 repo=dict(
531 repo=dict(
532 id=repo.repo_id,
532 id=repo.repo_id,
533 repo_name=repo.repo_name,
533 repo_name=repo.repo_name,
534 type=repo.repo_type,
534 type=repo.repo_type,
535 clone_uri=repo.clone_uri,
535 clone_uri=repo.clone_uri,
536 private=repo.private,
536 private=repo.private,
537 created_on=repo.created_on,
537 created_on=repo.created_on,
538 description=repo.description,
538 description=repo.description,
539 )
539 )
540 )
540 )
541
541
542 except Exception:
542 except Exception:
543 log.error(traceback.format_exc())
543 log.error(traceback.format_exc())
544 raise JSONRPCError('failed to create repository %s' % repo_name)
544 raise JSONRPCError('failed to create repository %s' % repo_name)
545
545
546 @HasPermissionAnyDecorator('hg.admin')
546 @HasPermissionAnyDecorator('hg.admin')
547 def fork_repo(self, apiuser, repoid):
548 repo = RepoModel().get_repo(repoid)
549 if repo is None:
550 raise JSONRPCError('unknown repository "%s"' % (repo or repoid))
551
552 RepoModel().create_fork(form_data, cur_user)
553
554
555 @HasPermissionAnyDecorator('hg.admin')
547 def delete_repo(self, apiuser, repo_name):
556 def delete_repo(self, apiuser, repo_name):
548 """
557 """
549 Deletes a given repository
558 Deletes a given repository
550
559
551 :param repo_name:
560 :param repo_name:
552 """
561 """
553 if not Repository.get_by_repo_name(repo_name):
562 if not Repository.get_by_repo_name(repo_name):
554 raise JSONRPCError("repo %s does not exist" % repo_name)
563 raise JSONRPCError("repo %s does not exist" % repo_name)
555 try:
564 try:
556 RepoModel().delete(repo_name)
565 RepoModel().delete(repo_name)
557 Session.commit()
566 Session.commit()
558 return dict(
567 return dict(
559 msg='Deleted repository %s' % repo_name
568 msg='Deleted repository %s' % repo_name
560 )
569 )
561 except Exception:
570 except Exception:
562 log.error(traceback.format_exc())
571 log.error(traceback.format_exc())
563 raise JSONRPCError('failed to delete repository %s' % repo_name)
572 raise JSONRPCError('failed to delete repository %s' % repo_name)
564
573
565 @HasPermissionAnyDecorator('hg.admin')
574 @HasPermissionAnyDecorator('hg.admin')
566 def grant_user_permission(self, apiuser, repo_name, username, perm):
575 def grant_user_permission(self, apiuser, repo_name, username, perm):
567 """
576 """
568 Grant permission for user on given repository, or update existing one
577 Grant permission for user on given repository, or update existing one
569 if found
578 if found
570
579
571 :param repo_name:
580 :param repo_name:
572 :param username:
581 :param username:
573 :param perm:
582 :param perm:
574 """
583 """
575
584
576 try:
585 try:
577 repo = Repository.get_by_repo_name(repo_name)
586 repo = Repository.get_by_repo_name(repo_name)
578 if repo is None:
587 if repo is None:
579 raise JSONRPCError('unknown repository %s' % repo)
588 raise JSONRPCError('unknown repository %s' % repo)
580
589
581 user = User.get_by_username(username)
590 user = User.get_by_username(username)
582 if user is None:
591 if user is None:
583 raise JSONRPCError('unknown user %s' % username)
592 raise JSONRPCError('unknown user %s' % username)
584
593
585 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
594 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
586
595
587 Session.commit()
596 Session.commit()
588 return dict(
597 return dict(
589 msg='Granted perm: %s for user: %s in repo: %s' % (
598 msg='Granted perm: %s for user: %s in repo: %s' % (
590 perm, username, repo_name
599 perm, username, repo_name
591 )
600 )
592 )
601 )
593 except Exception:
602 except Exception:
594 log.error(traceback.format_exc())
603 log.error(traceback.format_exc())
595 raise JSONRPCError(
604 raise JSONRPCError(
596 'failed to edit permission %(repo)s for %(user)s' % dict(
605 'failed to edit permission %(repo)s for %(user)s' % dict(
597 user=username, repo=repo_name
606 user=username, repo=repo_name
598 )
607 )
599 )
608 )
600
609
601 @HasPermissionAnyDecorator('hg.admin')
610 @HasPermissionAnyDecorator('hg.admin')
602 def revoke_user_permission(self, apiuser, repo_name, username):
611 def revoke_user_permission(self, apiuser, repo_name, username):
603 """
612 """
604 Revoke permission for user on given repository
613 Revoke permission for user on given repository
605
614
606 :param repo_name:
615 :param repo_name:
607 :param username:
616 :param username:
608 """
617 """
609
618
610 try:
619 try:
611 repo = Repository.get_by_repo_name(repo_name)
620 repo = Repository.get_by_repo_name(repo_name)
612 if repo is None:
621 if repo is None:
613 raise JSONRPCError('unknown repository %s' % repo)
622 raise JSONRPCError('unknown repository %s' % repo)
614
623
615 user = User.get_by_username(username)
624 user = User.get_by_username(username)
616 if user is None:
625 if user is None:
617 raise JSONRPCError('unknown user %s' % username)
626 raise JSONRPCError('unknown user %s' % username)
618
627
619 RepoModel().revoke_user_permission(repo=repo_name, user=username)
628 RepoModel().revoke_user_permission(repo=repo_name, user=username)
620
629
621 Session.commit()
630 Session.commit()
622 return dict(
631 return dict(
623 msg='Revoked perm for user: %s in repo: %s' % (
632 msg='Revoked perm for user: %s in repo: %s' % (
624 username, repo_name
633 username, repo_name
625 )
634 )
626 )
635 )
627 except Exception:
636 except Exception:
628 log.error(traceback.format_exc())
637 log.error(traceback.format_exc())
629 raise JSONRPCError(
638 raise JSONRPCError(
630 'failed to edit permission %(repo)s for %(user)s' % dict(
639 'failed to edit permission %(repo)s for %(user)s' % dict(
631 user=username, repo=repo_name
640 user=username, repo=repo_name
632 )
641 )
633 )
642 )
634
643
635 @HasPermissionAnyDecorator('hg.admin')
644 @HasPermissionAnyDecorator('hg.admin')
636 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
645 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
637 """
646 """
638 Grant permission for users group on given repository, or update
647 Grant permission for users group on given repository, or update
639 existing one if found
648 existing one if found
640
649
641 :param repo_name:
650 :param repo_name:
642 :param group_name:
651 :param group_name:
643 :param perm:
652 :param perm:
644 """
653 """
645
654
646 try:
655 try:
647 repo = Repository.get_by_repo_name(repo_name)
656 repo = Repository.get_by_repo_name(repo_name)
648 if repo is None:
657 if repo is None:
649 raise JSONRPCError('unknown repository %s' % repo)
658 raise JSONRPCError('unknown repository %s' % repo)
650
659
651 user_group = UsersGroup.get_by_group_name(group_name)
660 user_group = UsersGroup.get_by_group_name(group_name)
652 if user_group is None:
661 if user_group is None:
653 raise JSONRPCError('unknown users group %s' % user_group)
662 raise JSONRPCError('unknown users group %s' % user_group)
654
663
655 RepoModel().grant_users_group_permission(repo=repo_name,
664 RepoModel().grant_users_group_permission(repo=repo_name,
656 group_name=group_name,
665 group_name=group_name,
657 perm=perm)
666 perm=perm)
658
667
659 Session.commit()
668 Session.commit()
660 return dict(
669 return dict(
661 msg='Granted perm: %s for group: %s in repo: %s' % (
670 msg='Granted perm: %s for group: %s in repo: %s' % (
662 perm, group_name, repo_name
671 perm, group_name, repo_name
663 )
672 )
664 )
673 )
665 except Exception:
674 except Exception:
666 log.error(traceback.format_exc())
675 log.error(traceback.format_exc())
667 raise JSONRPCError(
676 raise JSONRPCError(
668 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
677 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
669 usersgr=group_name, repo=repo_name
678 usersgr=group_name, repo=repo_name
670 )
679 )
671 )
680 )
672
681
673 @HasPermissionAnyDecorator('hg.admin')
682 @HasPermissionAnyDecorator('hg.admin')
674 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
683 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
675 """
684 """
676 Revoke permission for users group on given repository
685 Revoke permission for users group on given repository
677
686
678 :param repo_name:
687 :param repo_name:
679 :param group_name:
688 :param group_name:
680 """
689 """
681
690
682 try:
691 try:
683 repo = Repository.get_by_repo_name(repo_name)
692 repo = Repository.get_by_repo_name(repo_name)
684 if repo is None:
693 if repo is None:
685 raise JSONRPCError('unknown repository %s' % repo)
694 raise JSONRPCError('unknown repository %s' % repo)
686
695
687 user_group = UsersGroup.get_by_group_name(group_name)
696 user_group = UsersGroup.get_by_group_name(group_name)
688 if user_group is None:
697 if user_group is None:
689 raise JSONRPCError('unknown users group %s' % user_group)
698 raise JSONRPCError('unknown users group %s' % user_group)
690
699
691 RepoModel().revoke_users_group_permission(repo=repo_name,
700 RepoModel().revoke_users_group_permission(repo=repo_name,
692 group_name=group_name)
701 group_name=group_name)
693
702
694 Session.commit()
703 Session.commit()
695 return dict(
704 return dict(
696 msg='Revoked perm for group: %s in repo: %s' % (
705 msg='Revoked perm for group: %s in repo: %s' % (
697 group_name, repo_name
706 group_name, repo_name
698 )
707 )
699 )
708 )
700 except Exception:
709 except Exception:
701 log.error(traceback.format_exc())
710 log.error(traceback.format_exc())
702 raise JSONRPCError(
711 raise JSONRPCError(
703 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
712 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
704 usersgr=group_name, repo=repo_name
713 usersgr=group_name, repo=repo_name
705 )
714 )
706 )
715 )
@@ -1,127 +1,128 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.feed
3 rhodecode.controllers.feed
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Feed controller for rhodecode
6 Feed controller for rhodecode
7
7
8 :created_on: Apr 23, 2010
8 :created_on: Apr 23, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27
27
28 from pylons import url, response, tmpl_context as c
28 from pylons import url, response, tmpl_context as c
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30
30
31 from rhodecode.lib.utils2 import safe_unicode
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32
33 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.base import BaseRepoController
35 from rhodecode.lib.base import BaseRepoController
34
36 from rhodecode.lib.diffs import DiffProcessor
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36
37
37 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
38
39
39
40
40 class FeedController(BaseRepoController):
41 class FeedController(BaseRepoController):
41
42
42 @LoginRequired(api_access=True)
43 @LoginRequired(api_access=True)
43 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 'repository.admin')
45 'repository.admin')
45 def __before__(self):
46 def __before__(self):
46 super(FeedController, self).__before__()
47 super(FeedController, self).__before__()
47 #common values for feeds
48 #common values for feeds
48 self.description = _('Changes on %s repository')
49 self.description = _('Changes on %s repository')
49 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
50 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
50 self.language = 'en-us'
51 self.language = 'en-us'
51 self.ttl = "5"
52 self.ttl = "5"
52 self.feed_nr = 10
53 self.feed_nr = 20
53
54
54 def _get_title(self, cs):
55 def _get_title(self, cs):
55 return "R%s:%s - %s" % (
56 return "%s" % (
56 cs.revision, cs.short_id, cs.message
57 h.shorter(cs.message, 160)
57 )
58 )
58
59
59 def __changes(self, cs):
60 def __changes(self, cs):
60 changes = []
61 changes = []
61
62
62 a = [safe_unicode(n.path) for n in cs.added]
63 diffprocessor = DiffProcessor(cs.diff())
63 if a:
64 stats = diffprocessor.prepare(inline_diff=False)
64 changes.append('\nA ' + '\nA '.join(a))
65 for st in stats:
65
66 st.update({'added': st['stats'][0],
66 m = [safe_unicode(n.path) for n in cs.changed]
67 'removed': st['stats'][1]})
67 if m:
68 changes.append('\n %(operation)s %(filename)s '
68 changes.append('\nM ' + '\nM '.join(m))
69 '(%(added)s lines added, %(removed)s lines removed)'
70 % st)
71 return changes
69
72
70 d = [safe_unicode(n.path) for n in cs.removed]
73 def __get_desc(self, cs):
71 if d:
74 desc_msg = []
72 changes.append('\nD ' + '\nD '.join(d))
75 desc_msg.append('%s %s %s:<br/>' % (cs.author, _('commited on'),
73
76 cs.date))
74 changes.append('</pre>')
77 desc_msg.append('<pre>')
75
78 desc_msg.append(cs.message)
76 return ''.join(changes)
79 desc_msg.append('\n')
80 desc_msg.extend(self.__changes(cs))
81 desc_msg.append('</pre>')
82 return desc_msg
77
83
78 def atom(self, repo_name):
84 def atom(self, repo_name):
79 """Produce an atom-1.0 feed via feedgenerator module"""
85 """Produce an atom-1.0 feed via feedgenerator module"""
80 feed = Atom1Feed(
86 feed = Atom1Feed(
81 title=self.title % repo_name,
87 title=self.title % repo_name,
82 link=url('summary_home', repo_name=repo_name,
88 link=url('summary_home', repo_name=repo_name,
83 qualified=True),
89 qualified=True),
84 description=self.description % repo_name,
90 description=self.description % repo_name,
85 language=self.language,
91 language=self.language,
86 ttl=self.ttl
92 ttl=self.ttl
87 )
93 )
88
94
89 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
95 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
90 desc_msg = []
91 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
92 desc_msg.append(self.__changes(cs))
93
94 feed.add_item(title=self._get_title(cs),
96 feed.add_item(title=self._get_title(cs),
95 link=url('changeset_home', repo_name=repo_name,
97 link=url('changeset_home', repo_name=repo_name,
96 revision=cs.raw_id, qualified=True),
98 revision=cs.raw_id, qualified=True),
97 author_name=cs.author,
99 author_name=cs.author,
98 description=''.join(desc_msg))
100 description=''.join(self.__get_desc(cs)),
101 pubdate=cs.date,
102 )
99
103
100 response.content_type = feed.mime_type
104 response.content_type = feed.mime_type
101 return feed.writeString('utf-8')
105 return feed.writeString('utf-8')
102
106
103 def rss(self, repo_name):
107 def rss(self, repo_name):
104 """Produce an rss2 feed via feedgenerator module"""
108 """Produce an rss2 feed via feedgenerator module"""
105 feed = Rss201rev2Feed(
109 feed = Rss201rev2Feed(
106 title=self.title % repo_name,
110 title=self.title % repo_name,
107 link=url('summary_home', repo_name=repo_name,
111 link=url('summary_home', repo_name=repo_name,
108 qualified=True),
112 qualified=True),
109 description=self.description % repo_name,
113 description=self.description % repo_name,
110 language=self.language,
114 language=self.language,
111 ttl=self.ttl
115 ttl=self.ttl
112 )
116 )
113
117
114 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
118 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 desc_msg = []
116 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
117 desc_msg.append(self.__changes(cs))
118
119 feed.add_item(title=self._get_title(cs),
119 feed.add_item(title=self._get_title(cs),
120 link=url('changeset_home', repo_name=repo_name,
120 link=url('changeset_home', repo_name=repo_name,
121 revision=cs.raw_id, qualified=True),
121 revision=cs.raw_id, qualified=True),
122 author_name=cs.author,
122 author_name=cs.author,
123 description=''.join(desc_msg),
123 description=''.join(self.__get_desc(cs)),
124 pubdate=cs.date,
124 )
125 )
125
126
126 response.content_type = feed.mime_type
127 response.content_type = feed.mime_type
127 return feed.writeString('utf-8')
128 return feed.writeString('utf-8')
@@ -1,532 +1,544 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import markupsafe
30 import markupsafe
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.helpers import escape
37 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.utils import EmptyChangeset
39
39
40
40
41 def wrap_to_table(str_):
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
46 </tr>
47 </table>''' % str_
47 </table>''' % str_
48
48
49
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
52 enable_comments=False):
53 """
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
55 proper message
56 """
56 """
57
57
58 if filenode_old is None:
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
60
61 if filenode_old.is_binary or filenode_new.is_binary:
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
63 stats = (0, 0)
64 size = 0
64 size = 0
65
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
75 stats = diff_processor.stat()
76 size = len(diff or '')
76 size = len(diff or '')
77 else:
77 else:
78 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
78 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
79 'diff menu to display this diff'))
79 'diff menu to display this diff'))
80 stats = (0, 0)
80 stats = (0, 0)
81 size = 0
81 size = 0
82 if not diff:
82 if not diff:
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
84 [filenode_new, filenode_old])
84 [filenode_new, filenode_old])
85 if submodules:
85 if submodules:
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
87 else:
87 else:
88 diff = wrap_to_table(_('No changes detected'))
88 diff = wrap_to_table(_('No changes detected'))
89
89
90 cs1 = filenode_old.changeset.raw_id
90 cs1 = filenode_old.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
92
92
93 return size, cs1, cs2, diff, stats
93 return size, cs1, cs2, diff, stats
94
94
95
95
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
97 """
97 """
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
99
99
100 :param ignore_whitespace: ignore whitespaces in diff
100 :param ignore_whitespace: ignore whitespaces in diff
101 """
101 """
102 # make sure we pass in default context
102 # make sure we pass in default context
103 context = context or 3
103 context = context or 3
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
105 [filenode_new, filenode_old])
106 if submodules:
106 if submodules:
107 return ''
107 return ''
108
108
109 for filenode in (filenode_old, filenode_new):
109 for filenode in (filenode_old, filenode_new):
110 if not isinstance(filenode, FileNode):
110 if not isinstance(filenode, FileNode):
111 raise VCSError("Given object should be FileNode object, not %s"
111 raise VCSError("Given object should be FileNode object, not %s"
112 % filenode.__class__)
112 % filenode.__class__)
113
113
114 repo = filenode_new.changeset.repository
114 repo = filenode_new.changeset.repository
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
117
117
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
119 ignore_whitespace, context)
119 ignore_whitespace, context)
120 return vcs_gitdiff
120 return vcs_gitdiff
121
121
122
122
123 class DiffProcessor(object):
123 class DiffProcessor(object):
124 """
124 """
125 Give it a unified diff and it returns a list of the files that were
125 Give it a unified diff and it returns a list of the files that were
126 mentioned in the diff together with a dict of meta information that
126 mentioned in the diff together with a dict of meta information that
127 can be used to render it in a HTML template.
127 can be used to render it in a HTML template.
128 """
128 """
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
130
130
131 def __init__(self, diff, differ='diff', format='udiff'):
131 def __init__(self, diff, differ='diff', format='gitdiff'):
132 """
132 """
133 :param diff: a text in diff format or generator
133 :param diff: a text in diff format or generator
134 :param format: format of diff passed, `udiff` or `gitdiff`
134 :param format: format of diff passed, `udiff` or `gitdiff`
135 """
135 """
136 if isinstance(diff, basestring):
136 if isinstance(diff, basestring):
137 diff = [diff]
137 diff = [diff]
138
138
139 self.__udiff = diff
139 self.__udiff = diff
140 self.__format = format
140 self.__format = format
141 self.adds = 0
141 self.adds = 0
142 self.removes = 0
142 self.removes = 0
143
143
144 if isinstance(self.__udiff, basestring):
144 if isinstance(self.__udiff, basestring):
145 self.lines = iter(self.__udiff.splitlines(1))
145 self.lines = iter(self.__udiff.splitlines(1))
146
146
147 elif self.__format == 'gitdiff':
147 elif self.__format == 'gitdiff':
148 udiff_copy = self.copy_iterator()
148 udiff_copy = self.copy_iterator()
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
150 else:
150 else:
151 udiff_copy = self.copy_iterator()
151 udiff_copy = self.copy_iterator()
152 self.lines = imap(self.escaper, udiff_copy)
152 self.lines = imap(self.escaper, udiff_copy)
153
153
154 # Select a differ.
154 # Select a differ.
155 if differ == 'difflib':
155 if differ == 'difflib':
156 self.differ = self._highlight_line_difflib
156 self.differ = self._highlight_line_difflib
157 else:
157 else:
158 self.differ = self._highlight_line_udiff
158 self.differ = self._highlight_line_udiff
159
159
160 def escaper(self, string):
160 def escaper(self, string):
161 return markupsafe.escape(string)
161 return markupsafe.escape(string)
162
162
163 def copy_iterator(self):
163 def copy_iterator(self):
164 """
164 """
165 make a fresh copy of generator, we should not iterate thru
165 make a fresh copy of generator, we should not iterate thru
166 an original as it's needed for repeating operations on
166 an original as it's needed for repeating operations on
167 this instance of DiffProcessor
167 this instance of DiffProcessor
168 """
168 """
169 self.__udiff, iterator_copy = tee(self.__udiff)
169 self.__udiff, iterator_copy = tee(self.__udiff)
170 return iterator_copy
170 return iterator_copy
171
171
172 def _extract_rev(self, line1, line2):
172 def _extract_rev(self, line1, line2):
173 """
173 """
174 Extract the filename and revision hint from a line.
174 Extract the operation (A/M/D), filename and revision hint from a line.
175 """
175 """
176
176
177 try:
177 try:
178 if line1.startswith('--- ') and line2.startswith('+++ '):
178 if line1.startswith('--- ') and line2.startswith('+++ '):
179 l1 = line1[4:].split(None, 1)
179 l1 = line1[4:].split(None, 1)
180 old_filename = (l1[0].replace('a/', '', 1)
180 old_filename = (l1[0].replace('a/', '', 1)
181 if len(l1) >= 1 else None)
181 if len(l1) >= 1 else None)
182 old_rev = l1[1] if len(l1) == 2 else 'old'
182 old_rev = l1[1] if len(l1) == 2 else 'old'
183
183
184 l2 = line2[4:].split(None, 1)
184 l2 = line2[4:].split(None, 1)
185 new_filename = (l2[0].replace('b/', '', 1)
185 new_filename = (l2[0].replace('b/', '', 1)
186 if len(l1) >= 1 else None)
186 if len(l1) >= 1 else None)
187 new_rev = l2[1] if len(l2) == 2 else 'new'
187 new_rev = l2[1] if len(l2) == 2 else 'new'
188
188
189 filename = (old_filename
189 filename = (old_filename
190 if old_filename != '/dev/null' else new_filename)
190 if old_filename != '/dev/null' else new_filename)
191
191
192 return filename, new_rev, old_rev
192 operation = 'D' if new_filename == '/dev/null' else None
193 if not operation:
194 operation = 'M' if old_filename != '/dev/null' else 'A'
195
196 return operation, filename, new_rev, old_rev
193 except (ValueError, IndexError):
197 except (ValueError, IndexError):
194 pass
198 pass
195
199
196 return None, None, None
200 return None, None, None, None
197
201
198 def _parse_gitdiff(self, diffiterator):
202 def _parse_gitdiff(self, diffiterator):
199 def line_decoder(l):
203 def line_decoder(l):
200 if l.startswith('+') and not l.startswith('+++'):
204 if l.startswith('+') and not l.startswith('+++'):
201 self.adds += 1
205 self.adds += 1
202 elif l.startswith('-') and not l.startswith('---'):
206 elif l.startswith('-') and not l.startswith('---'):
203 self.removes += 1
207 self.removes += 1
204 return l.decode('utf8', 'replace')
208 return l.decode('utf8', 'replace')
205
209
206 output = list(diffiterator)
210 output = list(diffiterator)
207 size = len(output)
211 size = len(output)
208
212
209 if size == 2:
213 if size == 2:
210 l = []
214 l = []
211 l.extend([output[0]])
215 l.extend([output[0]])
212 l.extend(output[1].splitlines(1))
216 l.extend(output[1].splitlines(1))
213 return map(line_decoder, l)
217 return map(line_decoder, l)
214 elif size == 1:
218 elif size == 1:
215 return map(line_decoder, output[0].splitlines(1))
219 return map(line_decoder, output[0].splitlines(1))
216 elif size == 0:
220 elif size == 0:
217 return []
221 return []
218
222
219 raise Exception('wrong size of diff %s' % size)
223 raise Exception('wrong size of diff %s' % size)
220
224
221 def _highlight_line_difflib(self, line, next_):
225 def _highlight_line_difflib(self, line, next_):
222 """
226 """
223 Highlight inline changes in both lines.
227 Highlight inline changes in both lines.
224 """
228 """
225
229
226 if line['action'] == 'del':
230 if line['action'] == 'del':
227 old, new = line, next_
231 old, new = line, next_
228 else:
232 else:
229 old, new = next_, line
233 old, new = next_, line
230
234
231 oldwords = re.split(r'(\W)', old['line'])
235 oldwords = re.split(r'(\W)', old['line'])
232 newwords = re.split(r'(\W)', new['line'])
236 newwords = re.split(r'(\W)', new['line'])
233
237
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
238 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
235
239
236 oldfragments, newfragments = [], []
240 oldfragments, newfragments = [], []
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
241 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
238 oldfrag = ''.join(oldwords[i1:i2])
242 oldfrag = ''.join(oldwords[i1:i2])
239 newfrag = ''.join(newwords[j1:j2])
243 newfrag = ''.join(newwords[j1:j2])
240 if tag != 'equal':
244 if tag != 'equal':
241 if oldfrag:
245 if oldfrag:
242 oldfrag = '<del>%s</del>' % oldfrag
246 oldfrag = '<del>%s</del>' % oldfrag
243 if newfrag:
247 if newfrag:
244 newfrag = '<ins>%s</ins>' % newfrag
248 newfrag = '<ins>%s</ins>' % newfrag
245 oldfragments.append(oldfrag)
249 oldfragments.append(oldfrag)
246 newfragments.append(newfrag)
250 newfragments.append(newfrag)
247
251
248 old['line'] = "".join(oldfragments)
252 old['line'] = "".join(oldfragments)
249 new['line'] = "".join(newfragments)
253 new['line'] = "".join(newfragments)
250
254
251 def _highlight_line_udiff(self, line, next_):
255 def _highlight_line_udiff(self, line, next_):
252 """
256 """
253 Highlight inline changes in both lines.
257 Highlight inline changes in both lines.
254 """
258 """
255 start = 0
259 start = 0
256 limit = min(len(line['line']), len(next_['line']))
260 limit = min(len(line['line']), len(next_['line']))
257 while start < limit and line['line'][start] == next_['line'][start]:
261 while start < limit and line['line'][start] == next_['line'][start]:
258 start += 1
262 start += 1
259 end = -1
263 end = -1
260 limit -= start
264 limit -= start
261 while -end <= limit and line['line'][end] == next_['line'][end]:
265 while -end <= limit and line['line'][end] == next_['line'][end]:
262 end -= 1
266 end -= 1
263 end += 1
267 end += 1
264 if start or end:
268 if start or end:
265 def do(l):
269 def do(l):
266 last = end + len(l['line'])
270 last = end + len(l['line'])
267 if l['action'] == 'add':
271 if l['action'] == 'add':
268 tag = 'ins'
272 tag = 'ins'
269 else:
273 else:
270 tag = 'del'
274 tag = 'del'
271 l['line'] = '%s<%s>%s</%s>%s' % (
275 l['line'] = '%s<%s>%s</%s>%s' % (
272 l['line'][:start],
276 l['line'][:start],
273 tag,
277 tag,
274 l['line'][start:last],
278 l['line'][start:last],
275 tag,
279 tag,
276 l['line'][last:]
280 l['line'][last:]
277 )
281 )
278 do(line)
282 do(line)
279 do(next_)
283 do(next_)
280
284
281 def _parse_udiff(self):
285 def _parse_udiff(self, inline_diff=True):
282 """
286 """
283 Parse the diff an return data for the template.
287 Parse the diff an return data for the template.
284 """
288 """
285 lineiter = self.lines
289 lineiter = self.lines
286 files = []
290 files = []
287 try:
291 try:
288 line = lineiter.next()
292 line = lineiter.next()
289 while 1:
293 while 1:
290 # continue until we found the old file
294 # continue until we found the old file
291 if not line.startswith('--- '):
295 if not line.startswith('--- '):
292 line = lineiter.next()
296 line = lineiter.next()
293 continue
297 continue
294
298
295 chunks = []
299 chunks = []
296 filename, old_rev, new_rev = \
300 stats = [0, 0]
301 operation, filename, old_rev, new_rev = \
297 self._extract_rev(line, lineiter.next())
302 self._extract_rev(line, lineiter.next())
298 files.append({
303 files.append({
299 'filename': filename,
304 'filename': filename,
300 'old_revision': old_rev,
305 'old_revision': old_rev,
301 'new_revision': new_rev,
306 'new_revision': new_rev,
302 'chunks': chunks
307 'chunks': chunks,
308 'operation': operation,
309 'stats': stats,
303 })
310 })
304
311
305 line = lineiter.next()
312 line = lineiter.next()
306 while line:
313 while line:
307 match = self._chunk_re.match(line)
314 match = self._chunk_re.match(line)
308 if not match:
315 if not match:
309 break
316 break
310
317
311 lines = []
318 lines = []
312 chunks.append(lines)
319 chunks.append(lines)
313
320
314 old_line, old_end, new_line, new_end = \
321 old_line, old_end, new_line, new_end = \
315 [int(x or 1) for x in match.groups()[:-1]]
322 [int(x or 1) for x in match.groups()[:-1]]
316 old_line -= 1
323 old_line -= 1
317 new_line -= 1
324 new_line -= 1
318 gr = match.groups()
325 gr = match.groups()
319 context = len(gr) == 5
326 context = len(gr) == 5
320 old_end += old_line
327 old_end += old_line
321 new_end += new_line
328 new_end += new_line
322
329
323 if context:
330 if context:
324 # skip context only if it's first line
331 # skip context only if it's first line
325 if int(gr[0]) > 1:
332 if int(gr[0]) > 1:
326 lines.append({
333 lines.append({
327 'old_lineno': '...',
334 'old_lineno': '...',
328 'new_lineno': '...',
335 'new_lineno': '...',
329 'action': 'context',
336 'action': 'context',
330 'line': line,
337 'line': line,
331 })
338 })
332
339
333 line = lineiter.next()
340 line = lineiter.next()
334
335 while old_line < old_end or new_line < new_end:
341 while old_line < old_end or new_line < new_end:
336 if line:
342 if line:
337 command, line = line[0], line[1:]
343 command, line = line[0], line[1:]
338 else:
344 else:
339 command = ' '
345 command = ' '
340 affects_old = affects_new = False
346 affects_old = affects_new = False
341
347
342 # ignore those if we don't expect them
348 # ignore those if we don't expect them
343 if command in '#@':
349 if command in '#@':
344 continue
350 continue
345 elif command == '+':
351 elif command == '+':
346 affects_new = True
352 affects_new = True
347 action = 'add'
353 action = 'add'
354 stats[0] += 1
348 elif command == '-':
355 elif command == '-':
349 affects_old = True
356 affects_old = True
350 action = 'del'
357 action = 'del'
358 stats[1] += 1
351 else:
359 else:
352 affects_old = affects_new = True
360 affects_old = affects_new = True
353 action = 'unmod'
361 action = 'unmod'
354
362
355 if line.find('No newline at end of file') != -1:
363 if line.find('No newline at end of file') != -1:
356 lines.append({
364 lines.append({
357 'old_lineno': '...',
365 'old_lineno': '...',
358 'new_lineno': '...',
366 'new_lineno': '...',
359 'action': 'context',
367 'action': 'context',
360 'line': line
368 'line': line
361 })
369 })
362
370
363 else:
371 else:
364 old_line += affects_old
372 old_line += affects_old
365 new_line += affects_new
373 new_line += affects_new
366 lines.append({
374 lines.append({
367 'old_lineno': affects_old and old_line or '',
375 'old_lineno': affects_old and old_line or '',
368 'new_lineno': affects_new and new_line or '',
376 'new_lineno': affects_new and new_line or '',
369 'action': action,
377 'action': action,
370 'line': line
378 'line': line
371 })
379 })
372
380
373 line = lineiter.next()
381 line = lineiter.next()
374
375 except StopIteration:
382 except StopIteration:
376 pass
383 pass
377
384
385 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
386 if inline_diff is False:
387 return sorted(files, key=sorter)
388
378 # highlight inline changes
389 # highlight inline changes
379 for _ in files:
390 for diff_data in files:
380 for chunk in chunks:
391 for chunk in diff_data['chunks']:
381 lineiter = iter(chunk)
392 lineiter = iter(chunk)
382 try:
393 try:
383 while 1:
394 while 1:
384 line = lineiter.next()
395 line = lineiter.next()
385 if line['action'] != 'unmod':
396 if line['action'] != 'unmod':
386 nextline = lineiter.next()
397 nextline = lineiter.next()
387 if nextline['action'] in ['unmod', 'context'] or \
398 if nextline['action'] in ['unmod', 'context'] or \
388 nextline['action'] == line['action']:
399 nextline['action'] == line['action']:
389 continue
400 continue
390 self.differ(line, nextline)
401 self.differ(line, nextline)
391 except StopIteration:
402 except StopIteration:
392 pass
403 pass
393
404
394 return files
405 return sorted(files, key=sorter)
395
406
396 def prepare(self):
407 def prepare(self, inline_diff=True):
397 """
408 """
398 Prepare the passed udiff for HTML rendering. It'l return a list
409 Prepare the passed udiff for HTML rendering. It'l return a list
399 of dicts
410 of dicts
400 """
411 """
401 return self._parse_udiff()
412 return self._parse_udiff(inline_diff=inline_diff)
402
413
403 def _safe_id(self, idstring):
414 def _safe_id(self, idstring):
404 """Make a string safe for including in an id attribute.
415 """Make a string safe for including in an id attribute.
405
416
406 The HTML spec says that id attributes 'must begin with
417 The HTML spec says that id attributes 'must begin with
407 a letter ([A-Za-z]) and may be followed by any number
418 a letter ([A-Za-z]) and may be followed by any number
408 of letters, digits ([0-9]), hyphens ("-"), underscores
419 of letters, digits ([0-9]), hyphens ("-"), underscores
409 ("_"), colons (":"), and periods (".")'. These regexps
420 ("_"), colons (":"), and periods (".")'. These regexps
410 are slightly over-zealous, in that they remove colons
421 are slightly over-zealous, in that they remove colons
411 and periods unnecessarily.
422 and periods unnecessarily.
412
423
413 Whitespace is transformed into underscores, and then
424 Whitespace is transformed into underscores, and then
414 anything which is not a hyphen or a character that
425 anything which is not a hyphen or a character that
415 matches \w (alphanumerics and underscore) is removed.
426 matches \w (alphanumerics and underscore) is removed.
416
427
417 """
428 """
418 # Transform all whitespace to underscore
429 # Transform all whitespace to underscore
419 idstring = re.sub(r'\s', "_", '%s' % idstring)
430 idstring = re.sub(r'\s', "_", '%s' % idstring)
420 # Remove everything that is not a hyphen or a member of \w
431 # Remove everything that is not a hyphen or a member of \w
421 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
432 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
422 return idstring
433 return idstring
423
434
424 def raw_diff(self):
435 def raw_diff(self):
425 """
436 """
426 Returns raw string as udiff
437 Returns raw string as udiff
427 """
438 """
428 udiff_copy = self.copy_iterator()
439 udiff_copy = self.copy_iterator()
429 if self.__format == 'gitdiff':
440 if self.__format == 'gitdiff':
430 udiff_copy = self._parse_gitdiff(udiff_copy)
441 udiff_copy = self._parse_gitdiff(udiff_copy)
431 return u''.join(udiff_copy)
442 return u''.join(udiff_copy)
432
443
433 def as_html(self, table_class='code-difftable', line_class='line',
444 def as_html(self, table_class='code-difftable', line_class='line',
434 new_lineno_class='lineno old', old_lineno_class='lineno new',
445 new_lineno_class='lineno old', old_lineno_class='lineno new',
435 code_class='code', enable_comments=False):
446 code_class='code', enable_comments=False, diff_lines=None):
436 """
447 """
437 Return udiff as html table with customized css classes
448 Return udiff as html table with customized css classes
438 """
449 """
439 def _link_to_if(condition, label, url):
450 def _link_to_if(condition, label, url):
440 """
451 """
441 Generates a link if condition is meet or just the label if not.
452 Generates a link if condition is meet or just the label if not.
442 """
453 """
443
454
444 if condition:
455 if condition:
445 return '''<a href="%(url)s">%(label)s</a>''' % {
456 return '''<a href="%(url)s">%(label)s</a>''' % {
446 'url': url,
457 'url': url,
447 'label': label
458 'label': label
448 }
459 }
449 else:
460 else:
450 return label
461 return label
451 diff_lines = self.prepare()
462 if diff_lines is None:
463 diff_lines = self.prepare()
452 _html_empty = True
464 _html_empty = True
453 _html = []
465 _html = []
454 _html.append('''<table class="%(table_class)s">\n''' % {
466 _html.append('''<table class="%(table_class)s">\n''' % {
455 'table_class': table_class
467 'table_class': table_class
456 })
468 })
457 for diff in diff_lines:
469 for diff in diff_lines:
458 for line in diff['chunks']:
470 for line in diff['chunks']:
459 _html_empty = False
471 _html_empty = False
460 for change in line:
472 for change in line:
461 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
473 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
462 'lc': line_class,
474 'lc': line_class,
463 'action': change['action']
475 'action': change['action']
464 })
476 })
465 anchor_old_id = ''
477 anchor_old_id = ''
466 anchor_new_id = ''
478 anchor_new_id = ''
467 anchor_old = "%(filename)s_o%(oldline_no)s" % {
479 anchor_old = "%(filename)s_o%(oldline_no)s" % {
468 'filename': self._safe_id(diff['filename']),
480 'filename': self._safe_id(diff['filename']),
469 'oldline_no': change['old_lineno']
481 'oldline_no': change['old_lineno']
470 }
482 }
471 anchor_new = "%(filename)s_n%(oldline_no)s" % {
483 anchor_new = "%(filename)s_n%(oldline_no)s" % {
472 'filename': self._safe_id(diff['filename']),
484 'filename': self._safe_id(diff['filename']),
473 'oldline_no': change['new_lineno']
485 'oldline_no': change['new_lineno']
474 }
486 }
475 cond_old = (change['old_lineno'] != '...' and
487 cond_old = (change['old_lineno'] != '...' and
476 change['old_lineno'])
488 change['old_lineno'])
477 cond_new = (change['new_lineno'] != '...' and
489 cond_new = (change['new_lineno'] != '...' and
478 change['new_lineno'])
490 change['new_lineno'])
479 if cond_old:
491 if cond_old:
480 anchor_old_id = 'id="%s"' % anchor_old
492 anchor_old_id = 'id="%s"' % anchor_old
481 if cond_new:
493 if cond_new:
482 anchor_new_id = 'id="%s"' % anchor_new
494 anchor_new_id = 'id="%s"' % anchor_new
483 ###########################################################
495 ###########################################################
484 # OLD LINE NUMBER
496 # OLD LINE NUMBER
485 ###########################################################
497 ###########################################################
486 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
498 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
487 'a_id': anchor_old_id,
499 'a_id': anchor_old_id,
488 'olc': old_lineno_class
500 'olc': old_lineno_class
489 })
501 })
490
502
491 _html.append('''%(link)s''' % {
503 _html.append('''%(link)s''' % {
492 'link': _link_to_if(True, change['old_lineno'],
504 'link': _link_to_if(True, change['old_lineno'],
493 '#%s' % anchor_old)
505 '#%s' % anchor_old)
494 })
506 })
495 _html.append('''</td>\n''')
507 _html.append('''</td>\n''')
496 ###########################################################
508 ###########################################################
497 # NEW LINE NUMBER
509 # NEW LINE NUMBER
498 ###########################################################
510 ###########################################################
499
511
500 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
512 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
501 'a_id': anchor_new_id,
513 'a_id': anchor_new_id,
502 'nlc': new_lineno_class
514 'nlc': new_lineno_class
503 })
515 })
504
516
505 _html.append('''%(link)s''' % {
517 _html.append('''%(link)s''' % {
506 'link': _link_to_if(True, change['new_lineno'],
518 'link': _link_to_if(True, change['new_lineno'],
507 '#%s' % anchor_new)
519 '#%s' % anchor_new)
508 })
520 })
509 _html.append('''</td>\n''')
521 _html.append('''</td>\n''')
510 ###########################################################
522 ###########################################################
511 # CODE
523 # CODE
512 ###########################################################
524 ###########################################################
513 comments = '' if enable_comments else 'no-comment'
525 comments = '' if enable_comments else 'no-comment'
514 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
526 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
515 'cc': code_class,
527 'cc': code_class,
516 'inc': comments
528 'inc': comments
517 })
529 })
518 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
530 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
519 'code': change['line']
531 'code': change['line']
520 })
532 })
521 _html.append('''\t</td>''')
533 _html.append('''\t</td>''')
522 _html.append('''\n</tr>\n''')
534 _html.append('''\n</tr>\n''')
523 _html.append('''</table>''')
535 _html.append('''</table>''')
524 if _html_empty:
536 if _html_empty:
525 return None
537 return None
526 return ''.join(_html)
538 return ''.join(_html)
527
539
528 def stat(self):
540 def stat(self):
529 """
541 """
530 Returns tuple of added, and removed lines for this instance
542 Returns tuple of added, and removed lines for this instance
531 """
543 """
532 return self.adds, self.removes
544 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now