Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeReleaseNotes
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 6
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 execute
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 showForm
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 showReleaseNotes
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
56
 shortenSummary
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
182
 isRelevant
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace MediaWiki\Extension\CodeReview\UI;
4
5use HTMLForm;
6use MediaWiki\Extension\CodeReview\Backend\CodeRevision;
7use SpecialPage;
8
9class 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            ->addHiddenField( 'title', $special->getPrefixedDBkey() )
71            ->setMethod( 'get' )
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}