1+ #!/usr/bin/env node
2+
3+ // TODO(vojta): pre-commit hook for validating messages
4+ // TODO(vojta): report errors, currently Q silence everything which really sucks
5+
6+ 'use strict' ;
7+
8+ var child = require ( 'child_process' ) ;
9+ var fs = require ( 'fs' ) ;
10+ var util = require ( 'util' ) ;
11+ var q = require ( 'qq' ) ;
12+
13+ var GIT_LOG_CMD = 'git log --grep="%s" -E --format=%s %s..HEAD' ;
14+ var GIT_TAG_CMD = 'git describe --tags --abbrev=0' ;
15+
16+ var HEADER_TPL = '<a name="%s"></a>\n# %s (%s)\n\n' ;
17+ var LINK_ISSUE = '[#%s](https://github.com/angular/angular.js/issues/%s)' ;
18+ var LINK_COMMIT = '[%s](https://github.com/angular/angular.js/commit/%s)' ;
19+
20+ var EMPTY_COMPONENT = '$$' ;
21+
22+
23+ var warn = function ( ) {
24+ console . log ( 'WARNING:' , util . format . apply ( null , arguments ) ) ;
25+ } ;
26+
27+
28+ var parseRawCommit = function ( raw ) {
29+ if ( ! raw ) return null ;
30+
31+ var lines = raw . split ( '\n' ) ;
32+ var msg = { } , match ;
33+
34+ msg . hash = lines . shift ( ) ;
35+ msg . subject = lines . shift ( ) ;
36+ msg . closes = [ ] ;
37+ msg . breaks = [ ] ;
38+
39+ lines . forEach ( function ( line ) {
40+ match = line . match ( / (?: C l o s e s | F i x e s ) \s # ( \d + ) / ) ;
41+ if ( match ) msg . closes . push ( parseInt ( match [ 1 ] ) ) ;
42+ } ) ;
43+
44+ match = raw . match ( / B R E A K I N G C H A N G E : ( [ \s \S ] * ) / ) ;
45+ if ( match ) {
46+ msg . breaking = match [ 1 ] ;
47+ }
48+
49+
50+ msg . body = lines . join ( '\n' ) ;
51+ match = msg . subject . match ( / ^ ( .* ) \( ( .* ) \) \: \s ( .* ) $ / ) ;
52+
53+ if ( ! match || ! match [ 1 ] || ! match [ 3 ] ) {
54+ warn ( 'Incorrect message: %s %s' , msg . hash , msg . subject ) ;
55+ return null ;
56+ }
57+
58+ msg . type = match [ 1 ] ;
59+ msg . component = match [ 2 ] ;
60+ msg . subject = match [ 3 ] ;
61+
62+ return msg ;
63+ } ;
64+
65+
66+ var linkToIssue = function ( issue ) {
67+ return util . format ( LINK_ISSUE , issue , issue ) ;
68+ } ;
69+
70+
71+ var linkToCommit = function ( hash ) {
72+ return util . format ( LINK_COMMIT , hash . substr ( 0 , 8 ) , hash ) ;
73+ } ;
74+
75+
76+ var currentDate = function ( ) {
77+ var now = new Date ( ) ;
78+ var pad = function ( i ) {
79+ return ( '0' + i ) . substr ( - 2 ) ;
80+ } ;
81+
82+ return util . format ( '%d-%s-%s' , now . getFullYear ( ) , pad ( now . getMonth ( ) + 1 ) , pad ( now . getDate ( ) ) ) ;
83+ } ;
84+
85+
86+ var printSection = function ( stream , title , section , printCommitLinks ) {
87+ printCommitLinks = printCommitLinks === undefined ? true : printCommitLinks ;
88+ var components = Object . getOwnPropertyNames ( section ) . sort ( ) ;
89+
90+ if ( ! components . length ) return ;
91+
92+ stream . write ( util . format ( '\n## %s\n\n' , title ) ) ;
93+
94+ components . forEach ( function ( name ) {
95+ var prefix = '-' ;
96+ var nested = section [ name ] . length > 1 ;
97+
98+ if ( name !== EMPTY_COMPONENT ) {
99+ if ( nested ) {
100+ stream . write ( util . format ( '- **%s:**\n' , name ) ) ;
101+ prefix = ' -' ;
102+ } else {
103+ prefix = util . format ( '- **%s:**' , name ) ;
104+ }
105+ }
106+
107+ section [ name ] . forEach ( function ( commit ) {
108+ if ( printCommitLinks ) {
109+ stream . write ( util . format ( '%s %s\n (%s' , prefix , commit . subject , linkToCommit ( commit . hash ) ) ) ;
110+ if ( commit . closes . length ) {
111+ stream . write ( ',\n ' + commit . closes . map ( linkToIssue ) . join ( ', ' ) ) ;
112+ }
113+ stream . write ( ')\n' ) ;
114+ } else {
115+ stream . write ( util . format ( '%s %s\n' , prefix , commit . subject ) ) ;
116+ }
117+ } ) ;
118+ } ) ;
119+
120+ stream . write ( '\n' ) ;
121+ } ;
122+
123+
124+ var readGitLog = function ( grep , from ) {
125+ var deferred = q . defer ( ) ;
126+
127+ // TODO(vojta): if it's slow, use spawn and stream it instead
128+ child . exec ( util . format ( GIT_LOG_CMD , grep , '%H%n%s%n%b%n==END==' , from ) , function ( code , stdout , stderr ) {
129+ var commits = [ ] ;
130+
131+ stdout . split ( '\n==END==\n' ) . forEach ( function ( rawCommit ) {
132+ var commit = parseRawCommit ( rawCommit ) ;
133+ if ( commit ) commits . push ( commit ) ;
134+ } ) ;
135+
136+ deferred . resolve ( commits ) ;
137+ } ) ;
138+
139+ return deferred . promise ;
140+ } ;
141+
142+
143+ var writeChangelog = function ( stream , commits , version ) {
144+ var sections = {
145+ fix : { } ,
146+ feat : { } ,
147+ perf : { } ,
148+ breaks : { }
149+ } ;
150+
151+ sections . breaks [ EMPTY_COMPONENT ] = [ ] ;
152+
153+ commits . forEach ( function ( commit ) {
154+ var section = sections [ commit . type ] ;
155+ var component = commit . component || EMPTY_COMPONENT ;
156+
157+ if ( section ) {
158+ section [ component ] = section [ component ] || [ ] ;
159+ section [ component ] . push ( commit ) ;
160+ }
161+
162+ if ( commit . breaking ) {
163+ sections . breaks [ component ] = sections . breaks [ component ] || [ ] ;
164+ sections . breaks [ component ] . push ( {
165+ subject : util . format ( "due to %s,\n %s" , linkToCommit ( commit . hash ) , commit . breaking ) ,
166+ hash : commit . hash ,
167+ closes : [ ]
168+ } ) ;
169+ }
170+ } ) ;
171+
172+ stream . write ( util . format ( HEADER_TPL , version , version , currentDate ( ) ) ) ;
173+ printSection ( stream , 'Features' , sections . feat ) ;
174+ printSection ( stream , 'Performance Improvements' , sections . perf ) ;
175+ printSection ( stream , 'Breaking Changes' , sections . breaks , false ) ;
176+ } ;
177+
178+
179+ var getPreviousTag = function ( ) {
180+ var deferred = q . defer ( ) ;
181+ child . exec ( GIT_TAG_CMD , function ( code , stdout , stderr ) {
182+ if ( code ) deferred . reject ( 'Cannot get the previous tag.' ) ;
183+ else deferred . resolve ( stdout . replace ( '\n' , '' ) ) ;
184+ } ) ;
185+ return deferred . promise ;
186+ } ;
187+
188+
189+ var generate = function ( version , file ) {
190+
191+ getPreviousTag ( ) . then ( function ( tag ) {
192+ console . log ( 'Reading git log since' , tag ) ;
193+ readGitLog ( '^fix|^feat|^perf|BREAKING' , tag ) . then ( function ( commits ) {
194+ console . log ( 'Parsed' , commits . length , 'commits' ) ;
195+ console . log ( 'Generating changelog to' , file || 'stdout' , '(' , version , ')' ) ;
196+ writeChangelog ( file ? fs . createWriteStream ( file ) : process . stdout , commits , version ) ;
197+ } ) ;
198+ } ) ;
199+ } ;
200+
201+
202+ // publish for testing
203+ exports . parseRawCommit = parseRawCommit ;
204+ exports . printSection = printSection ;
205+
206+ // hacky start if not run by jasmine :-D
207+ if ( process . argv . join ( '' ) . indexOf ( 'jasmine-node' ) === - 1 ) {
208+ generate ( process . argv [ 2 ] , process . argv [ 3 ] ) ;
209+ }
0 commit comments