99#include "mailmap.h"
1010#include "shortlog.h"
1111#include "parse-options.h"
12+ #include "trailer.h"
1213
1314static char const * const shortlog_usage [] = {
1415 N_ ("git shortlog [<options>] [<revision-range>] [[--] <path>...]" ),
@@ -49,12 +50,12 @@ static int compare_by_list(const void *a1, const void *a2)
4950}
5051
5152static void insert_one_record (struct shortlog * log ,
52- const char * author ,
53+ const char * ident ,
5354 const char * oneline )
5455{
5556 struct string_list_item * item ;
5657
57- item = string_list_insert (& log -> list , author );
58+ item = string_list_insert (& log -> list , ident );
5859
5960 if (log -> summary )
6061 item -> util = (void * )(UTIL_TO_INT (item ) + 1 );
@@ -97,8 +98,8 @@ static void insert_one_record(struct shortlog *log,
9798 }
9899}
99100
100- static int parse_stdin_author (struct shortlog * log ,
101- struct strbuf * out , const char * in )
101+ static int parse_ident (struct shortlog * log ,
102+ struct strbuf * out , const char * in )
102103{
103104 const char * mailbuf , * namebuf ;
104105 size_t namelen , maillen ;
@@ -122,18 +123,33 @@ static int parse_stdin_author(struct shortlog *log,
122123
123124static void read_from_stdin (struct shortlog * log )
124125{
125- struct strbuf author = STRBUF_INIT ;
126- struct strbuf mapped_author = STRBUF_INIT ;
126+ struct strbuf ident = STRBUF_INIT ;
127+ struct strbuf mapped_ident = STRBUF_INIT ;
127128 struct strbuf oneline = STRBUF_INIT ;
128129 static const char * author_match [2 ] = { "Author: " , "author " };
129130 static const char * committer_match [2 ] = { "Commit: " , "committer " };
130131 const char * * match ;
131132
132- match = log -> committer ? committer_match : author_match ;
133- while (strbuf_getline_lf (& author , stdin ) != EOF ) {
133+ if (HAS_MULTI_BITS (log -> groups ))
134+ die (_ ("using multiple --group options with stdin is not supported" ));
135+
136+ switch (log -> groups ) {
137+ case SHORTLOG_GROUP_AUTHOR :
138+ match = author_match ;
139+ break ;
140+ case SHORTLOG_GROUP_COMMITTER :
141+ match = committer_match ;
142+ break ;
143+ case SHORTLOG_GROUP_TRAILER :
144+ die (_ ("using --group=trailer with stdin is not supported" ));
145+ default :
146+ BUG ("unhandled shortlog group" );
147+ }
148+
149+ while (strbuf_getline_lf (& ident , stdin ) != EOF ) {
134150 const char * v ;
135- if (!skip_prefix (author .buf , match [0 ], & v ) &&
136- !skip_prefix (author .buf , match [1 ], & v ))
151+ if (!skip_prefix (ident .buf , match [0 ], & v ) &&
152+ !skip_prefix (ident .buf , match [1 ], & v ))
137153 continue ;
138154 while (strbuf_getline_lf (& oneline , stdin ) != EOF &&
139155 oneline .len )
@@ -142,45 +158,157 @@ static void read_from_stdin(struct shortlog *log)
142158 !oneline .len )
143159 ; /* discard blanks */
144160
145- strbuf_reset (& mapped_author );
146- if (parse_stdin_author (log , & mapped_author , v ) < 0 )
161+ strbuf_reset (& mapped_ident );
162+ if (parse_ident (log , & mapped_ident , v ) < 0 )
147163 continue ;
148164
149- insert_one_record (log , mapped_author .buf , oneline .buf );
165+ insert_one_record (log , mapped_ident .buf , oneline .buf );
150166 }
151- strbuf_release (& author );
152- strbuf_release (& mapped_author );
167+ strbuf_release (& ident );
168+ strbuf_release (& mapped_ident );
153169 strbuf_release (& oneline );
154170}
155171
172+ struct strset_item {
173+ struct hashmap_entry ent ;
174+ char value [FLEX_ARRAY ];
175+ };
176+
177+ struct strset {
178+ struct hashmap map ;
179+ };
180+
181+ #define STRSET_INIT { { NULL } }
182+
183+ static int strset_item_hashcmp (const void * hash_data ,
184+ const struct hashmap_entry * entry ,
185+ const struct hashmap_entry * entry_or_key ,
186+ const void * keydata )
187+ {
188+ const struct strset_item * a , * b ;
189+
190+ a = container_of (entry , const struct strset_item , ent );
191+ if (keydata )
192+ return strcmp (a -> value , keydata );
193+
194+ b = container_of (entry_or_key , const struct strset_item , ent );
195+ return strcmp (a -> value , b -> value );
196+ }
197+
198+ /*
199+ * Adds "str" to the set if it was not already present; returns true if it was
200+ * already there.
201+ */
202+ static int strset_check_and_add (struct strset * ss , const char * str )
203+ {
204+ unsigned int hash = strhash (str );
205+ struct strset_item * item ;
206+
207+ if (!ss -> map .table )
208+ hashmap_init (& ss -> map , strset_item_hashcmp , NULL , 0 );
209+
210+ if (hashmap_get_from_hash (& ss -> map , hash , str ))
211+ return 1 ;
212+
213+ FLEX_ALLOC_STR (item , value , str );
214+ hashmap_entry_init (& item -> ent , hash );
215+ hashmap_add (& ss -> map , & item -> ent );
216+ return 0 ;
217+ }
218+
219+ static void strset_clear (struct strset * ss )
220+ {
221+ if (!ss -> map .table )
222+ return ;
223+ hashmap_free_entries (& ss -> map , struct strset_item , ent );
224+ }
225+
226+ static void insert_records_from_trailers (struct shortlog * log ,
227+ struct strset * dups ,
228+ struct commit * commit ,
229+ struct pretty_print_context * ctx ,
230+ const char * oneline )
231+ {
232+ struct trailer_iterator iter ;
233+ const char * commit_buffer , * body ;
234+ struct strbuf ident = STRBUF_INIT ;
235+
236+ /*
237+ * Using format_commit_message("%B") would be simpler here, but
238+ * this saves us copying the message.
239+ */
240+ commit_buffer = logmsg_reencode (commit , NULL , ctx -> output_encoding );
241+ body = strstr (commit_buffer , "\n\n" );
242+ if (!body )
243+ return ;
244+
245+ trailer_iterator_init (& iter , body );
246+ while (trailer_iterator_advance (& iter )) {
247+ const char * value = iter .val .buf ;
248+
249+ if (!string_list_has_string (& log -> trailers , iter .key .buf ))
250+ continue ;
251+
252+ strbuf_reset (& ident );
253+ if (!parse_ident (log , & ident , value ))
254+ value = ident .buf ;
255+
256+ if (strset_check_and_add (dups , value ))
257+ continue ;
258+ insert_one_record (log , value , oneline );
259+ }
260+ trailer_iterator_release (& iter );
261+
262+ strbuf_release (& ident );
263+ unuse_commit_buffer (commit , commit_buffer );
264+ }
265+
156266void shortlog_add_commit (struct shortlog * log , struct commit * commit )
157267{
158- struct strbuf author = STRBUF_INIT ;
268+ struct strbuf ident = STRBUF_INIT ;
159269 struct strbuf oneline = STRBUF_INIT ;
270+ struct strset dups = STRSET_INIT ;
160271 struct pretty_print_context ctx = {0 };
161- const char * fmt ;
272+ const char * oneline_str ;
162273
163274 ctx .fmt = CMIT_FMT_USERFORMAT ;
164275 ctx .abbrev = log -> abbrev ;
165276 ctx .print_email_subject = 1 ;
166277 ctx .date_mode .type = DATE_NORMAL ;
167278 ctx .output_encoding = get_log_output_encoding ();
168279
169- fmt = log -> committer ?
170- (log -> email ? "%cN <%cE>" : "%cN" ) :
171- (log -> email ? "%aN <%aE>" : "%aN" );
172-
173- format_commit_message (commit , fmt , & author , & ctx );
174280 if (!log -> summary ) {
175281 if (log -> user_format )
176282 pretty_print_commit (& ctx , commit , & oneline );
177283 else
178284 format_commit_message (commit , "%s" , & oneline , & ctx );
179285 }
286+ oneline_str = oneline .len ? oneline .buf : "<none>" ;
287+
288+ if (log -> groups & SHORTLOG_GROUP_AUTHOR ) {
289+ strbuf_reset (& ident );
290+ format_commit_message (commit ,
291+ log -> email ? "%aN <%aE>" : "%aN" ,
292+ & ident , & ctx );
293+ if (!HAS_MULTI_BITS (log -> groups ) ||
294+ !strset_check_and_add (& dups , ident .buf ))
295+ insert_one_record (log , ident .buf , oneline_str );
296+ }
297+ if (log -> groups & SHORTLOG_GROUP_COMMITTER ) {
298+ strbuf_reset (& ident );
299+ format_commit_message (commit ,
300+ log -> email ? "%cN <%cE>" : "%cN" ,
301+ & ident , & ctx );
302+ if (!HAS_MULTI_BITS (log -> groups ) ||
303+ !strset_check_and_add (& dups , ident .buf ))
304+ insert_one_record (log , ident .buf , oneline_str );
305+ }
306+ if (log -> groups & SHORTLOG_GROUP_TRAILER ) {
307+ insert_records_from_trailers (log , & dups , commit , & ctx , oneline_str );
308+ }
180309
181- insert_one_record (log , author .buf , oneline .len ? oneline .buf : "<none>" );
182-
183- strbuf_release (& author );
310+ strset_clear (& dups );
311+ strbuf_release (& ident );
184312 strbuf_release (& oneline );
185313}
186314
@@ -241,6 +369,28 @@ static int parse_wrap_args(const struct option *opt, const char *arg, int unset)
241369 return 0 ;
242370}
243371
372+ static int parse_group_option (const struct option * opt , const char * arg , int unset )
373+ {
374+ struct shortlog * log = opt -> value ;
375+ const char * field ;
376+
377+ if (unset ) {
378+ log -> groups = 0 ;
379+ string_list_clear (& log -> trailers , 0 );
380+ } else if (!strcasecmp (arg , "author" ))
381+ log -> groups |= SHORTLOG_GROUP_AUTHOR ;
382+ else if (!strcasecmp (arg , "committer" ))
383+ log -> groups |= SHORTLOG_GROUP_COMMITTER ;
384+ else if (skip_prefix (arg , "trailer:" , & field )) {
385+ log -> groups |= SHORTLOG_GROUP_TRAILER ;
386+ string_list_append (& log -> trailers , field );
387+ } else
388+ return error (_ ("unknown group type: %s" ), arg );
389+
390+ return 0 ;
391+ }
392+
393+
244394void shortlog_init (struct shortlog * log )
245395{
246396 memset (log , 0 , sizeof (* log ));
@@ -251,6 +401,8 @@ void shortlog_init(struct shortlog *log)
251401 log -> wrap = DEFAULT_WRAPLEN ;
252402 log -> in1 = DEFAULT_INDENT1 ;
253403 log -> in2 = DEFAULT_INDENT2 ;
404+ log -> trailers .strdup_strings = 1 ;
405+ log -> trailers .cmp = strcasecmp ;
254406}
255407
256408int cmd_shortlog (int argc , const char * * argv , const char * prefix )
@@ -260,8 +412,9 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
260412 int nongit = !startup_info -> have_repository ;
261413
262414 const struct option options [] = {
263- OPT_BOOL ('c' , "committer" , & log .committer ,
264- N_ ("Group by committer rather than author" )),
415+ OPT_BIT ('c' , "committer" , & log .groups ,
416+ N_ ("Group by committer rather than author" ),
417+ SHORTLOG_GROUP_COMMITTER ),
265418 OPT_BOOL ('n' , "numbered" , & log .sort_by_number ,
266419 N_ ("sort output according to the number of commits per author" )),
267420 OPT_BOOL ('s' , "summary" , & log .summary ,
@@ -271,6 +424,8 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
271424 OPT_CALLBACK_F ('w' , NULL , & log , N_ ("<w>[,<i1>[,<i2>]]" ),
272425 N_ ("Linewrap output" ), PARSE_OPT_OPTARG ,
273426 & parse_wrap_args ),
427+ OPT_CALLBACK (0 , "group" , & log , N_ ("field" ),
428+ N_ ("Group by field" ), parse_group_option ),
274429 OPT_END (),
275430 };
276431
@@ -311,6 +466,10 @@ int cmd_shortlog(int argc, const char **argv, const char *prefix)
311466 log .abbrev = rev .abbrev ;
312467 log .file = rev .diffopt .file ;
313468
469+ if (!log .groups )
470+ log .groups = SHORTLOG_GROUP_AUTHOR ;
471+ string_list_sort (& log .trailers );
472+
314473 /* assume HEAD if from a tty */
315474 if (!nongit && !rev .pending .nr && isatty (0 ))
316475 add_head_to_pending (& rev );
0 commit comments