Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 128 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
CodeReleaseNotes | |
0.00% |
0 / 128 |
|
0.00% |
0 / 6 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
execute | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
showForm | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
2 | |||
showReleaseNotes | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
56 | |||
shortenSummary | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
182 | |||
isRelevant | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CodeReview\UI; |
4 | |
5 | use HTMLForm; |
6 | use MediaWiki\Extension\CodeReview\Backend\CodeRevision; |
7 | use SpecialPage; |
8 | |
9 | class CodeReleaseNotes extends CodeView { |
10 | public function __construct( $repo ) { |
11 | global $wgRequest; |
12 | parent::__construct( $repo ); |
13 | $this->mPath = htmlspecialchars( trim( $wgRequest->getVal( 'path', '' ) ) ); |
14 | if ( strlen( $this->mPath ) && $this->mPath[0] !== '/' ) { |
15 | // make sure this is a valid path |
16 | $this->mPath = "/{$this->mPath}"; |
17 | } |
18 | // remove last slash |
19 | $this->mPath = preg_replace( '/\/$/', '', $this->mPath ); |
20 | $this->mStartRev = $wgRequest->getIntOrNull( 'startrev' ); |
21 | $this->mEndRev = $wgRequest->getIntOrNull( 'endrev' ); |
22 | } |
23 | |
24 | public function execute() { |
25 | if ( !$this->mRepo ) { |
26 | $view = new CodeRepoListView(); |
27 | $view->execute(); |
28 | return; |
29 | } |
30 | $this->showForm(); |
31 | |
32 | # Show notes if we have at least a starting revision |
33 | if ( $this->mStartRev ) { |
34 | $this->showReleaseNotes(); |
35 | } |
36 | } |
37 | |
38 | protected function showForm() { |
39 | global $wgOut, $wgScript; |
40 | $special = SpecialPage::getTitleFor( 'Code', $this->mRepo->getName() . '/releasenotes' ); |
41 | $formDescriptor = [ |
42 | 'textbox1' => [ |
43 | 'type' => 'text', |
44 | 'name' => 'startrev', |
45 | 'id' => 'startrev', |
46 | 'label' => wfMessage( 'code-release-startrev' )->text(), |
47 | 'size' => 10, |
48 | 'value' => $this->mStartRev |
49 | ], |
50 | 'textbox2' => [ |
51 | 'type' => 'text', |
52 | 'name' => 'endrev', |
53 | 'id' => 'endrev', |
54 | 'label' => wfMessage( 'code-release-endrev' )->text(), |
55 | 'size' => 10, |
56 | 'value' => $this->mEndRev |
57 | ], |
58 | 'textbox3' => [ |
59 | 'type' => 'text', |
60 | 'name' => 'path', |
61 | 'id' => 'path', |
62 | 'label' => wfMessage( 'code-pathsearch-path' )->text(), |
63 | 'size' => 45, |
64 | 'value' => $this->mPath |
65 | ] |
66 | ]; |
67 | |
68 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $wgOut->getContext() ); |
69 | $htmlForm |
70 | ->setMethod( 'get' ) |
71 | ->setTitle( $special ) |
72 | ->setAction( $wgScript ) |
73 | ->setSubmitText( wfMessage( 'allpagessubmit' )->text() ) |
74 | ->setWrapperLegend( wfMessage( 'code-release-legend' )->text() ) |
75 | ->prepareForm() |
76 | ->displayForm( false ); |
77 | } |
78 | |
79 | protected function showReleaseNotes() { |
80 | global $wgOut; |
81 | $dbr = wfGetDB( DB_REPLICA ); |
82 | $where = []; |
83 | if ( $this->mEndRev ) { |
84 | $where[] = 'cr_id BETWEEN ' . intval( $this->mStartRev ) . ' AND ' . |
85 | intval( $this->mEndRev ); |
86 | } else { |
87 | $where[] = 'cr_id >= ' . intval( $this->mStartRev ); |
88 | } |
89 | if ( $this->mPath ) { |
90 | $where['cr_path'] = $this->mPath; |
91 | } |
92 | # Select commits within this range... |
93 | $res = $dbr->select( |
94 | [ 'code_rev', 'code_tags' ], |
95 | [ 'cr_message', 'cr_author', 'cr_id', 'ct_tag AS rnotes' ], |
96 | array_merge( [ |
97 | // this repo |
98 | 'cr_repo_id' => $this->mRepo->getId(), |
99 | // not reverted/deferred/fixme |
100 | "cr_status NOT IN('reverted','deferred','fixme')", |
101 | "cr_message != ''", |
102 | ], $where ), |
103 | __METHOD__, |
104 | [ 'ORDER BY' => 'cr_id DESC' ], |
105 | # Tagged for release notes? |
106 | [ 'code_tags' => [ 'LEFT JOIN', |
107 | 'ct_repo_id = cr_repo_id AND ct_rev_id = cr_id AND ct_tag = "release-notes"' ] |
108 | ] |
109 | ); |
110 | $wgOut->addHTML( '<ul>' ); |
111 | # Output any relevant seeming commits... |
112 | foreach ( $res as $row ) { |
113 | $summary = htmlspecialchars( $row->cr_message ); |
114 | # Add this commit summary if needed. |
115 | if ( $row->rnotes || $this->isRelevant( $summary ) ) { |
116 | # Keep it short if possible... |
117 | $summary = $this->shortenSummary( $summary ); |
118 | # Anything left? (this can happen with some heuristics) |
119 | if ( $summary ) { |
120 | // Newlines -> <br /> |
121 | $summary = str_replace( "\n", '<br />', $summary ); |
122 | $wgOut->addHTML( '<li>' ); |
123 | $wgOut->addHTML( |
124 | $this->codeCommentLinkerHtml->link( $summary ) . " <i>(" . |
125 | htmlspecialchars( $row->cr_author ) . |
126 | ', ' . $this->codeCommentLinkerHtml->link( "r{$row->cr_id}" ) . ")</i>" |
127 | ); |
128 | $wgOut->addHTML( "</li>\n" ); |
129 | } |
130 | } |
131 | } |
132 | $wgOut->addHTML( '</ul>' ); |
133 | } |
134 | |
135 | private function shortenSummary( $summary, $first = true ) { |
136 | # Astericks often used for point-by-point bullets |
137 | if ( preg_match( '/(^|\n) ?\*/', $summary ) ) { |
138 | $blurbs = explode( '*', $summary ); |
139 | # Double newlines separate importance generally |
140 | } elseif ( strpos( $summary, "\n\n" ) !== false ) { |
141 | $blurbs = explode( "\n\n", $summary ); |
142 | } else { |
143 | return trim( $summary ); |
144 | } |
145 | # Clean up items |
146 | $blurbs = array_map( 'trim', $blurbs ); |
147 | # Filter out any garbage |
148 | $blurbs = array_filter( $blurbs ); |
149 | |
150 | # Doesn't start with '*' and has some length? |
151 | # If so, then assume that the top bit is important. |
152 | if ( count( $blurbs ) ) { |
153 | $header = strpos( ltrim( $summary ), '*' ) !== 0 && str_word_count( $blurbs[0] ) >= 5; |
154 | } else { |
155 | $header = false; |
156 | } |
157 | # Keep it short if possible... |
158 | if ( count( $blurbs ) > 1 ) { |
159 | $summary = []; |
160 | foreach ( $blurbs as $blurb ) { |
161 | # Always show the first bit |
162 | if ( $header && $first && count( $summary ) == 0 ) { |
163 | $summary[] = $this->shortenSummary( $blurb, true ); |
164 | # Is this bit important? Does it mention a revision? |
165 | } elseif ( $this->isRelevant( $blurb ) || preg_match( '/\br(\d+)\b/', $blurb ) ) { |
166 | $bit = $this->shortenSummary( $blurb, false ); |
167 | if ( $bit ) { |
168 | $summary[] = $bit; |
169 | } |
170 | } |
171 | } |
172 | $summary = implode( "\n", $summary ); |
173 | } else { |
174 | $summary = implode( "\n", $blurbs ); |
175 | } |
176 | return $summary; |
177 | } |
178 | |
179 | /** |
180 | * Quick relevance tests (these *should* be over-inclusive a little if anything) |
181 | * |
182 | * @param string $summary |
183 | * @param bool $whole Are we looking at the whole summary or an aspect of it? |
184 | * @return bool|int |
185 | */ |
186 | private function isRelevant( $summary, $whole = true ) { |
187 | # Mentioned a bug? |
188 | if ( preg_match( CodeRevision::BUG_REFERENCE, $summary ) ) { |
189 | return true; |
190 | } |
191 | # Mentioned a config var? |
192 | if ( preg_match( '/\b\$[we]g[0-9a-z]{3,50}\b/i', $summary ) ) { |
193 | return true; |
194 | } |
195 | # Sanity check: summary cannot be *too* short to be useful |
196 | $words = str_word_count( $summary ); |
197 | if ( mb_strlen( $summary ) < 40 || $words <= 5 ) { |
198 | return false; |
199 | } |
200 | # All caps words (like "BREAKING CHANGE"/magic words)? |
201 | if ( preg_match( '/\b[A-Z]{6,30}\b/', $summary ) ) { |
202 | return true; |
203 | } |
204 | # Random keywords |
205 | if ( preg_match( |
206 | '/\b(wiki|HTML\d|CSS\d|UTF-?8|(Apache|PHP|CGI|Java|Perl|Python|\w+SQL) ?\d?\.?\d?)\b/i', |
207 | $summary ) |
208 | ) { |
209 | return true; |
210 | } |
211 | # Are we looking at the whole summary or an aspect of it? |
212 | if ( $whole ) { |
213 | # List of items? |
214 | return preg_match( '/(^|\n) ?\*/', $summary ); |
215 | } else { |
216 | return true; |
217 | } |
218 | } |
219 | } |