├── .gitignore ├── htdocs ├── sage_logo_trac_v2.png └── theme.css ├── templates ├── prefs_ssh_keys.html └── ticket_box.html └── plugins ├── trac_plugin_search_branch.py ├── sshkeys.py └── ticket_branch.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | log 3 | files 4 | -------------------------------------------------------------------------------- /htdocs/sage_logo_trac_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagemath/sage_trac/master/htdocs/sage_logo_trac_v2.png -------------------------------------------------------------------------------- /templates/prefs_ssh_keys.html: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 9 | SSH keys 10 | 11 | 12 |

You can associate up to 256 ssh keys to your trac account. 13 | You can copy-paste them to the text area below, each on its own line.

14 |

Paste your ssh keys here:
15 | 16 |

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /plugins/trac_plugin_search_branch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Search for the "Branch" custom field 3 | 4 | Only exact matches are returned 5 | """ 6 | 7 | from datetime import datetime 8 | from trac.core import * 9 | from trac.search import ISearchSource, search_to_sql 10 | from trac.util.datefmt import utc, from_utimestamp 11 | from tracrpc.api import IXMLRPCHandler 12 | 13 | class BranchSearchModule(Component): 14 | """Search the "Branch" custom field""" 15 | 16 | implements(ISearchSource) 17 | implements(IXMLRPCHandler) 18 | 19 | # IXMLRPCHandler methods 20 | def xmlrpc_namespace(self): 21 | return 'search' 22 | 23 | def xmlrpc_methods(self): 24 | yield ('SEARCH_VIEW', ((list,str),), self.branch) 25 | 26 | def branch(self, req, terms): 27 | return self.get_search_results(req, [terms], ['branch']) 28 | 29 | # ISearchSource methods 30 | def get_search_filters(self, req): 31 | if 'CHANGESET_VIEW' in req.perm: 32 | yield ('branch', 'Branch') 33 | 34 | def get_search_results(self, req, terms, filters): 35 | # Note: output looks like this: 36 | # yield (12345, 'title', from_utimestamp(0), 'owner', 'search match') 37 | if not 'branch' in filters: 38 | return 39 | try: 40 | branch_name = terms[0].encode('ascii') 41 | except UnicodeDecodeError: 42 | return 43 | query_string = """ 44 | SELECT t.id AS ticket, summary, t.time as time, owner, c.value AS branch 45 | FROM ticket t, ticket_custom c 46 | WHERE t.id = c.ticket AND c.name = %s AND c.value = %s 47 | ORDER BY t.id 48 | """ 49 | with self.env.db_query as db: 50 | cursor = db.cursor() 51 | cursor.execute(query_string, ['branch', branch_name]) 52 | for ticket, summary, time, owner, branch in cursor: 53 | yield (int(ticket), summary, from_utimestamp(time), owner, branch) 54 | 55 | 56 | -------------------------------------------------------------------------------- /htdocs/theme.css: -------------------------------------------------------------------------------- 1 | :link, :visited, dt em, .milestone .info h2 em, 2 | #content.build h2.config :link, #content.build h2.config :visited, 3 | .plugin h3 a { 4 | color: #22f; 5 | } 6 | 7 | /* blue ticket box */ 8 | #ticket { 9 | background-color: #eef; 10 | border: 1px solid #42426f; 11 | } 12 | 13 | #ticket > h2 { 14 | color: #42426f; 15 | } 16 | 17 | #ticket > h2 .trac-type { 18 | color: #42426f; 19 | } 20 | 21 | #ticket table.properties { 22 | border-top: 1px solid #42426f; 23 | } 24 | 25 | #ticket table.properties tr { 26 | border-bottom: 1px dotted #dfdeee; 27 | } 28 | 29 | #ticket table.properties th { 30 | color: #42426f; 31 | } 32 | 33 | #ticket table.properties th.missing { 34 | color: #aaaadb; 35 | } 36 | 37 | #ticket table.properties .description { 38 | border-top: 1px solid #42426f; 39 | } 40 | 41 | #ticket .description h3 { 42 | border-bottom: 1px solid #42426f; 43 | color: #42426f; 44 | } 45 | 46 | #ticket .date { 47 | color: #42426f; 48 | } 49 | 50 | /* color code links to tickets */ 51 | .closed { 52 | color: #0c0; 53 | font-weight: bold; 54 | border-bottom: 1px solid; 55 | } 56 | 57 | #ticket > h2 .trac-id-closed { 58 | color: #0c0; 59 | border-bottom: 1px solid; 60 | text-decoration: line-through; 61 | font-size: 145%; 62 | vertical-align: middle; 63 | margin: 0 .4em 0 0; 64 | } 65 | 66 | .positive_review { 67 | color: #0c0; 68 | font-weight: bold; 69 | border-bottom: 1px solid; 70 | } 71 | 72 | #ticket > h2 .trac-id-positive_review { 73 | color: #0c0; 74 | border-bottom: 1px solid; 75 | font-size: 145%; 76 | vertical-align: middle; 77 | margin: 0 .4em 0 0; 78 | } 79 | 80 | .new { 81 | color: #33f; 82 | font-weight: bold; 83 | } 84 | 85 | #ticket > h2 .trac-id-new { 86 | color: #33f; 87 | font-size: 145%; 88 | vertical-align: middle; 89 | margin: 0 .4em 0 0; 90 | } 91 | 92 | .needs_info { 93 | color: #c00; 94 | font-weight: bold; 95 | } 96 | 97 | #ticket > h2 .trac-id-needs_info { 98 | color: #c00; 99 | font-size: 145%; 100 | vertical-align: middle; 101 | margin: 0 .4em 0 0; 102 | } 103 | 104 | .needs_work { 105 | color: #c00; 106 | font-weight: bold; 107 | } 108 | 109 | #ticket > h2 .trac-id-needs_work { 110 | color: #c00; 111 | font-size: 145%; 112 | vertical-align: middle; 113 | margin: 0 .4em 0 0; 114 | } 115 | 116 | .needs_review { 117 | color: #f90; 118 | font-weight: bold; 119 | } 120 | 121 | #ticket > h2 .trac-id-needs_review { 122 | color: #f90; 123 | font-size: 145%; 124 | vertical-align: middle; 125 | margin: 0 .4em 0 0; 126 | } 127 | 128 | blockquote.citation { 129 | border-color: #44b; 130 | } 131 | 132 | .citation blockquote.citation { 133 | border-color: #b44; 134 | } 135 | 136 | .citation .citation blockquote.citation { 137 | border-color: #4b4; 138 | } 139 | 140 | .citation .citation .citation blockquote.citation { 141 | border-color: #55c; 142 | } 143 | 144 | #ticket table.properties td[headers="h_commit"] { 145 | text-overflow: ellipsis; 146 | overflow: hidden; 147 | } 148 | -------------------------------------------------------------------------------- /templates/ticket_box.html: -------------------------------------------------------------------------------- 1 | 14 |
20 | 21 |
22 |

Opened ${pretty_dateinfo(ticket.time)}

23 |

Closed ${pretty_dateinfo(closetime)}

24 |

25 | Last modified ${pretty_dateinfo(ticket.changetime)}

26 |

(ticket not yet created)

27 |
28 | 29 | 30 | 31 | 35 |
36 | 37 | 38 | 39 |
40 | 41 | 42 |

43 | 44 | 46 | #${ticket.id} 47 | 48 | 49 | 50 | ${'status' in fields_map and fields[fields_map['status']].rendered or ticket.status} 51 | 52 | 53 | ${'type' in fields_map and fields[fields_map['type']].rendered or ticket.type} 54 | 55 | 56 | (${'resolution' in fields_map and fields[fields_map['resolution']].rendered or ticket.resolution}) 57 | 58 |

59 | 60 |

61 | $ticket.summary 62 | 63 | 64 | 65 | — at Initial Version 66 | 67 | 68 | — at Version $version 69 | 70 | 71 |

72 | 73 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 94 | 106 | 107 | 108 |
Reported by:$v_reporterOwned by:$v_owner
92 | ${field.label or field.name}: 93 | 98 | 99 | 100 | ${pretty_dateinfo(field.dateinfo, field.format)} 101 | ${field.rendered} 102 | ${ticket[field.name]} 103 | 104 | 105 |
109 |
110 |

111 | Description 112 | 114 | (last modified by ${authorinfo(description_change.author)}) 115 | 116 |

117 | 118 | 119 |
121 |
122 | 123 | 124 |
125 |
126 |
127 | ${wiki_to_html(context, ticket.description, escape_newlines=preserve_newlines)} 128 |
129 |
130 |
131 |
132 | -------------------------------------------------------------------------------- /plugins/sshkeys.py: -------------------------------------------------------------------------------- 1 | from trac.core import * 2 | from trac.web.chrome import * 3 | from trac.util.translation import gettext as _ 4 | from trac.prefs import IPreferencePanelProvider 5 | from trac.admin.api import IAdminCommandProvider 6 | from trac.util.text import printout 7 | from tracrpc.api import IXMLRPCHandler 8 | import subprocess, os 9 | 10 | _home = '/home/www-data' 11 | _gitolite_keydir = os.path.join(_home, 'gitolite', 'keydir') 12 | _gitolite_update = os.path.join(_home, 'bin', 'gitolite-update') 13 | 14 | class UserDataStore(Component): 15 | def save_data(self, user, dictionary): 16 | """ 17 | Saves user data for user. 18 | """ 19 | self._create_table() 20 | with self.env.db_transaction as db: 21 | cursor = db.cursor() 22 | for key, value in dictionary.iteritems(): 23 | cursor.execute('DELETE FROM "user_data_store" WHERE "user"=%s', (user,)) 24 | cursor.execute('INSERT INTO "user_data_store" VALUES (%s, %s, %s)', (user, key, value)) 25 | 26 | def get_data(self, user): 27 | """ 28 | Returns a dictionary with all data keys 29 | """ 30 | self._create_table() 31 | with self.env.db_query as db: 32 | cursor = db.cursor() 33 | cursor.execute('SELECT key, value FROM "user_data_store" WHERE "user"=%s', (user,)) 34 | return {key:value for key, value in cursor} 35 | 36 | def get_data_all_users(self): 37 | """ 38 | Returns a dictionary with all data keys 39 | """ 40 | self._create_table() 41 | return_value = {} 42 | with self.env.db_query as db: 43 | cursor = db.cursor() 44 | cursor.execute('SELECT "user", key, value FROM "user_data_store"') 45 | for user, key, value in cursor: 46 | if return_value.has_key(user): 47 | return_value[user][key] = value 48 | else: 49 | return_value[user] = {key: value} 50 | return return_value 51 | 52 | def _create_table(self): 53 | with self.env.db_transaction as db: 54 | cursor = db.cursor() 55 | cursor.execute('SELECT * FROM information_schema.tables WHERE "table_name"=%s', ('user_data_store',)) 56 | if not cursor.rowcount: 57 | cursor.execute('CREATE TABLE "user_data_store" ( "user" text, key text, value text, UNIQUE ( "user", key ) )') 58 | 59 | class SshKeysPlugin(Component): 60 | implements(IPreferencePanelProvider, IAdminCommandProvider, IXMLRPCHandler) 61 | 62 | def __init__(self): 63 | self._user_data_store = UserDataStore(self.compmgr) 64 | 65 | # IPreferencePanelProvider methods 66 | def get_preference_panels(self, req): 67 | yield ('sshkeys', _('SSH keys')) 68 | 69 | def render_preference_panel(self, req, panel): 70 | if req.method == 'POST': 71 | new_ssh_keys = set(key.strip() for key in req.args.get('ssh_keys').splitlines()) 72 | if new_ssh_keys: 73 | self.setkeys(req, new_ssh_keys) 74 | add_notice(req, 'Your ssh key has been saved.') 75 | req.redirect(req.href.prefs(panel or None)) 76 | 77 | return 'prefs_ssh_keys.html', self._user_data_store.get_data(req.authname) 78 | 79 | # IAdminCommandProvider methods 80 | def get_admin_commands(self): 81 | yield ('sshkeys listusers', '', 82 | 'Get a list of users that have a SSH key registered', 83 | None, self._do_listusers) 84 | yield ('sshkeys dumpkey', '', 85 | "export the 's SSH key to stdout", 86 | None, self._do_dump_key) 87 | 88 | # AdminCommandProvider boilerplate 89 | 90 | def _do_listusers(self): 91 | for user in self._listusers(): 92 | printout(user) 93 | 94 | def _do_dump_key(self, user): 95 | printout(self._getkeys(user)) 96 | 97 | # Gitolite exporting 98 | def _export_to_gitolite(self, user, keys): 99 | for i,key in enumerate(keys): 100 | d = hex(i)[2:] 101 | while len(d) < 2: 102 | d = '0'+d 103 | f = open(os.path.join(_gitolite_keydir, d, user+'.pub'), 'w') 104 | f.write(key) 105 | f.close() 106 | for i in range(len(keys), len(self._getkeys(user))): 107 | d = hex(i)[2:] 108 | while len(d) < 2: 109 | d = '0'+d 110 | os.unlink(os.path.join(_gitolite_keydir, d, user+'.pub')) 111 | process = subprocess.Popen(_gitolite_update) 112 | process.wait() 113 | 114 | # general functionality 115 | def _listusers(self): 116 | all_data = self._user_data_store.get_data_all_users() 117 | for user, data in all_data.iteritems(): 118 | if data.has_key('ssh_keys'): 119 | yield user 120 | 121 | def _getkeys(self, user): 122 | ret = self._user_data_store.get_data(user) 123 | if not ret: return [] 124 | return ret['ssh_keys'].splitlines() 125 | 126 | def _setkeys(self, user, keys): 127 | self._export_to_gitolite(user, keys) 128 | self._user_data_store.save_data(user, {'ssh_keys': '\n'.join(keys)}) 129 | 130 | # RPC boilerplate 131 | def listusers(self, req): 132 | return list(self._listusers()) 133 | 134 | def getkeys(self, req): 135 | return self._getkeys(req.authname) 136 | 137 | def setkeys(self, req, keys): 138 | if req.authname == 'anonymous': 139 | raise TracError('cannot set ssh keys for anonymous users') 140 | keys = set(keys) 141 | if len(keys) > 0x100: 142 | add_warning(req, 'We only support using your first 256 ssh keys.') 143 | return self._setkeys(req.authname, keys) 144 | 145 | def addkeys(self, req, keys): 146 | new_keys = self.getkeys(req) 147 | new_keys.extend(keys) 148 | self.setkeys(req, new_keys) 149 | 150 | def addkey(self, req, key): 151 | self.addkeys(req, (key,)) 152 | 153 | # IXMLRPCHandler methods 154 | def xmlrpc_namespace(self): 155 | return "sshkeys" 156 | 157 | def xmlrpc_methods(self): 158 | yield (None, ((list,),), self.listusers) 159 | yield (None, ((list,),), self.getkeys) 160 | yield (None, ((None,list),), self.setkeys) 161 | yield (None, ((None,list),), self.addkeys) 162 | yield (None, ((None,str),), self.addkey) 163 | -------------------------------------------------------------------------------- /plugins/ticket_branch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from genshi.builder import tag 4 | from genshi.filters import Transformer 5 | 6 | from trac.core import * 7 | from trac.web.api import ITemplateStreamFilter 8 | from trac.ticket.api import ITicketManipulator 9 | 10 | import subprocess 11 | import os.path 12 | 13 | import pygit2 14 | 15 | MASTER_BRANCH = u'develop' 16 | MAX_NEW_COMMITS = 10 17 | 18 | GIT_BASE_URL = 'http://git.sagemath.org/sage.git/' 19 | GIT_COMMIT_URL = GIT_BASE_URL + 'commit/?id={commit}' 20 | GIT_DIFF_URL = GIT_BASE_URL + 'diff/?id={commit}' 21 | GIT_DIFF_RANGE_URL = GIT_BASE_URL + 'diff/?id={branch}&id2={base}' 22 | GIT_LOG_RANGE_URL = GIT_BASE_URL + 'log/?h={branch}&qt=range&q={base}..{branch}' 23 | 24 | GIT_SPECIAL_MERGES = ('GIT_FASTFORWARD', 'GIT_UPTODATE', 'GIT_FAILED_MERGE') 25 | for _merge in GIT_SPECIAL_MERGES: 26 | globals()[_merge] = _merge 27 | 28 | TRAC_SIGNATURE = pygit2.Signature('trac', 'trac@sagemath.org') 29 | 30 | FILTER = Transformer('//td[@headers="h_branch"]') 31 | FILTER_TEXT = Transformer('//td[@headers="h_branch"]/text()') 32 | 33 | class TicketBranch(Component): 34 | """ 35 | A Sage specific plugin which formats the ``branch`` field of a ticket and 36 | applies changes to the ``branch`` field to the git repository. 37 | """ 38 | implements(ITemplateStreamFilter) 39 | implements(ITicketManipulator) 40 | 41 | def __init__(self, *args, **kwargs): 42 | Component.__init__(self, *args, **kwargs) 43 | self.git_dir = self.config.get("trac","repository_dir","") 44 | if not self.git_dir: 45 | raise TracError("[trac] repository_dir is not set in the config file") 46 | self._master = None 47 | 48 | def filter_stream(self, req, method, filename, stream, data): 49 | """ 50 | Reformat the ``branch`` field of a ticket to show the history of the 51 | linked branch. 52 | """ 53 | branch = data.get('ticket', {'branch':None})['branch'] 54 | if filename != 'ticket.html' or not branch: 55 | return stream 56 | 57 | def error_filters(error): 58 | return FILTER.attr("class", "needs_work"), FILTER.attr("title", error) 59 | 60 | def apply_filters(filters): 61 | s = stream 62 | for filter in filters: 63 | s |= filter 64 | return s 65 | 66 | def error(error, filters=()): 67 | filters = tuple(filters)+error_filters(error) 68 | return apply_filters(filters) 69 | 70 | branch = branch.strip() 71 | 72 | branch = self._git.lookup_branch(branch) 73 | if branch is None: 74 | return error("branch does not exist") 75 | else: 76 | branch = branch.get_object() 77 | 78 | filters = [FILTER.append(tag.a('(Commits)', 79 | href=GIT_LOG_RANGE_URL.format( 80 | base=self.master_sha1, 81 | branch=branch.hex) 82 | ))] 83 | 84 | tmp = self._get_cache(branch) 85 | if tmp is None: 86 | try: 87 | tmp = self._merge(branch) 88 | except pygit2.GitError: 89 | tmp = GIT_FAILED_MERGE 90 | 91 | self._set_cache(branch, tmp) 92 | 93 | if tmp == GIT_FAILED_MERGE: 94 | return error("does not merge cleanly", filters) 95 | elif tmp == GIT_FASTFORWARD: 96 | filters.append(FILTER_TEXT.wrap(tag.a(class_="positive_review", 97 | href=GIT_DIFF_RANGE_URL.format( 98 | base=self.master_sha1, 99 | branch=branch.hex) 100 | ))) 101 | elif tmp == GIT_UPTODATE: 102 | filters.append(FILTER.attr("class", "positive_review")) 103 | filters.append(FILTER.attr("title", "already merged")) 104 | else: 105 | filters.append(FILTER_TEXT.wrap(tag.a(class_="positive_review", 106 | href=GIT_DIFF_URL.format(commit=tmp.hex)))) 107 | 108 | return apply_filters(filters) 109 | 110 | def _get_cache(self, branch): 111 | self._create_table() 112 | with self.env.db_query as db: 113 | cursor = db.cursor() 114 | cursor.execute('SELECT base, tmp FROM "merge_store" WHERE target=%s', (branch.hex,)) 115 | try: 116 | base, tmp = cursor.next() 117 | except StopIteration: 118 | return None 119 | if base != self.master_sha1: 120 | self._drop_table() 121 | return None 122 | if tmp in GIT_SPECIAL_MERGES: 123 | return tmp 124 | return self._git.get(tmp) 125 | 126 | def _set_cache(self, branch, tmp): 127 | self._create_table() 128 | with self.env.db_transaction as db: 129 | cursor = db.cursor() 130 | cursor.execute('DELETE FROM "merge_store" WHERE target=%s', (branch.hex,)) 131 | if tmp not in GIT_SPECIAL_MERGES: 132 | tmp = tmp.hex 133 | cursor.execute('INSERT INTO "merge_store" VALUES (%s, %s, %s)', (self.master_sha1, branch.hex, tmp)) 134 | 135 | @property 136 | def master_sha1(self): 137 | return self._git.lookup_branch(MASTER_BRANCH).get_object().hex 138 | 139 | def _create_table(self): 140 | with self.env.db_transaction as db: 141 | cursor = db.cursor() 142 | cursor.execute('SELECT * FROM information_schema.tables WHERE "table_name"=%s', ('merge_store',)) 143 | if not cursor.rowcount: 144 | cursor.execute('CREATE TABLE "merge_store" ( base text, target text, tmp text, PRIMARY KEY ( target ), UNIQUE ( target, tmp ) )') 145 | 146 | def _drop_table(self): 147 | with self.env.db_transaction as db: 148 | cursor = db.cursor() 149 | cursor.execute('SELECT * FROM information_schema.tables WHERE "table_name"=%s', ('merge_store',)) 150 | if cursor.rowcount: 151 | cursor.execute('DROP TABLE "merge_store"') 152 | 153 | def _merge(self, branch): 154 | import tempfile 155 | tmpdir = tempfile.mkdtemp() 156 | 157 | try: 158 | # libgit2/pygit2 are ridiculously slow when cloning local paths 159 | subprocess.call(['git', 'clone', self.git_dir, tmpdir, '--branch=%s'%MASTER_BRANCH]) 160 | 161 | repo = pygit2.Repository(tmpdir) 162 | merge = repo.merge(branch.oid) 163 | if merge.is_fastforward: 164 | ret = GIT_FASTFORWARD 165 | elif merge.is_uptodate: 166 | ret = GIT_UPTODATE 167 | else: 168 | # record the files that changed 169 | changed = set() 170 | for file, s in repo.status().items(): 171 | if s != pygit2.GIT_STATUS_INDEX_DELETED: 172 | changed.add(file) 173 | file = os.path.dirname(file) 174 | while file: 175 | changed.add(file) 176 | file = os.path.dirname(file) 177 | 178 | # write the merged tree 179 | # this will error if merge isn't clean 180 | merge_tree = repo.index.write_tree() 181 | 182 | # write objects to main git repo 183 | def recursive_write(tree, path=''): 184 | for obj in tree: 185 | new_path = os.path.join(path, obj.name) 186 | if new_path in changed: 187 | obj = repo.get(obj.oid) 188 | if isinstance(obj, pygit2.Tree): 189 | recursive_write(obj, new_path) 190 | else: 191 | self._git.write(pygit2.GIT_OBJ_BLOB, obj.read_raw()) 192 | return self._git.write(pygit2.GIT_OBJ_TREE, tree.read_raw()) 193 | merge_tree = recursive_write(repo.get(merge_tree)) 194 | 195 | ret = self._git.create_commit( 196 | None, # don't update any refs 197 | TRAC_SIGNATURE, # author 198 | TRAC_SIGNATURE, # committer 199 | 'Temporary merge of %s into %s'%(branch.hex, repo.head.get_object().hex), # merge message 200 | merge_tree, # commit's tree 201 | [repo.head.get_object().oid, branch.oid], # parents 202 | ) 203 | finally: 204 | import shutil 205 | shutil.rmtree(tmpdir) 206 | return ret 207 | 208 | @property 209 | def _git(self): 210 | try: 211 | return self.__git 212 | except AttributeError: 213 | self.__git = pygit2.Repository(self.git_dir) 214 | return self.__git 215 | 216 | def _valid_commit(self, val): 217 | if not isinstance(val, basestring): 218 | return 219 | if len(val) != 40: 220 | return 221 | try: 222 | int(val, 16) 223 | return val.lower() 224 | except ValueError: 225 | return 226 | 227 | def log_table(self, new_commit, limit=float('inf'), ignore=[]): 228 | walker = self._git.walk(self._git[new_commit].oid, 229 | pygit2.GIT_SORT_TOPOLOGICAL | pygit2.GIT_SORT_TIME) 230 | 231 | for b in ignore: 232 | c = self._git.lookup_branch(b) 233 | if c is None: 234 | c = self._git.get(b) 235 | else: 236 | c = c.get_object() 237 | if c is not None: 238 | walker.hide(c.oid) 239 | 240 | table = [] 241 | 242 | for commit in walker: 243 | if len(table) >= limit: 244 | break 245 | short_sha1 = commit.hex[:7] 246 | title = commit.message.splitlines() 247 | if title: 248 | title = title[0] 249 | else: 250 | title = u'' 251 | table.append( 252 | u'||[%s %s]||{{{%s}}}||'%( 253 | GIT_COMMIT_URL.format(commit=commit.hex), 254 | short_sha1, 255 | title)) 256 | table.reverse() 257 | return table 258 | 259 | # doesn't actually do anything, according to the api 260 | def prepare_ticket(self, req, ticket, fields, actions): pass 261 | 262 | # hack changes into validate_ticket, since api is currently silly 263 | def validate_ticket(self, req, ticket): 264 | branch = ticket['branch'] 265 | old_commit = self._valid_commit(ticket['commit']) 266 | if branch: 267 | ticket['branch'] = branch = branch.strip() 268 | commit = self._git.lookup_branch(branch) 269 | if commit is None: 270 | commit = ticket['commit'] = u'' 271 | else: 272 | commit = ticket['commit'] = unicode(commit.get_object().hex) 273 | else: 274 | commit = ticket['commit'] = u'' 275 | 276 | if (req.args.get('preview') is None and 277 | req.args.get('id') is not None and 278 | commit and 279 | commit != old_commit): 280 | ignore = {MASTER_BRANCH} 281 | if old_commit is not None: 282 | ignore.add(old_commit) 283 | try: 284 | table = self.log_table(commit, limit=MAX_NEW_COMMITS+1,ignore=ignore) 285 | except (pygit2.GitError, KeyError): 286 | return [] 287 | if len(table) > MAX_NEW_COMMITS: 288 | header = u'Last {0} new commits:'.format(MAX_NEW_COMMITS) 289 | table = table[:MAX_NEW_COMMITS] 290 | else: 291 | header = u'New commits:' 292 | if table: 293 | comment = req.args.get('comment', u'').splitlines() 294 | if comment: 295 | comment.append(u'----') 296 | comment.append(header) 297 | comment.extend(table) 298 | req.args['comment'] = u'\n'.join(comment) 299 | 300 | return [] 301 | --------------------------------------------------------------------------------