Commit 102061bd8b0b174e1cf811dfd35641d8a9e4eba3

Authored by Simon Glass
1 parent 757f64a89b

patman: Avoid duplicate sign-offs

Keep track of all Signed-off-by tags in a commit and silently suppress any
duplicates.

Signed-off-by: Simon Glass <sjg@chromium.org>

Showing 3 changed files with 23 additions and 2 deletions Inline Diff

1 # Copyright (c) 2011 The Chromium OS Authors. 1 # Copyright (c) 2011 The Chromium OS Authors.
2 # 2 #
3 # SPDX-License-Identifier: GPL-2.0+ 3 # SPDX-License-Identifier: GPL-2.0+
4 # 4 #
5 5
6 What is this? 6 What is this?
7 ============= 7 =============
8 8
9 This tool is a Python script which: 9 This tool is a Python script which:
10 - Creates patch directly from your branch 10 - Creates patch directly from your branch
11 - Cleans them up by removing unwanted tags 11 - Cleans them up by removing unwanted tags
12 - Inserts a cover letter with change lists 12 - Inserts a cover letter with change lists
13 - Runs the patches through checkpatch.pl and its own checks 13 - Runs the patches through checkpatch.pl and its own checks
14 - Optionally emails them out to selected people 14 - Optionally emails them out to selected people
15 15
16 It is intended to automate patch creation and make it a less 16 It is intended to automate patch creation and make it a less
17 error-prone process. It is useful for U-Boot and Linux work so far, 17 error-prone process. It is useful for U-Boot and Linux work so far,
18 since it uses the checkpatch.pl script. 18 since it uses the checkpatch.pl script.
19 19
20 It is configured almost entirely by tags it finds in your commits. 20 It is configured almost entirely by tags it finds in your commits.
21 This means that you can work on a number of different branches at 21 This means that you can work on a number of different branches at
22 once, and keep the settings with each branch rather than having to 22 once, and keep the settings with each branch rather than having to
23 git format-patch, git send-email, etc. with the correct parameters 23 git format-patch, git send-email, etc. with the correct parameters
24 each time. So for example if you put: 24 each time. So for example if you put:
25 25
26 Series-to: fred.blogs@napier.co.nz 26 Series-to: fred.blogs@napier.co.nz
27 27
28 in one of your commits, the series will be sent there. 28 in one of your commits, the series will be sent there.
29 29
30 In Linux this will also call get_maintainer.pl on each of your 30 In Linux this will also call get_maintainer.pl on each of your
31 patches automatically. 31 patches automatically.
32 32
33 33
34 How to use this tool 34 How to use this tool
35 ==================== 35 ====================
36 36
37 This tool requires a certain way of working: 37 This tool requires a certain way of working:
38 38
39 - Maintain a number of branches, one for each patch series you are 39 - Maintain a number of branches, one for each patch series you are
40 working on 40 working on
41 - Add tags into the commits within each branch to indicate where the 41 - Add tags into the commits within each branch to indicate where the
42 series should be sent, cover letter, version, etc. Most of these are 42 series should be sent, cover letter, version, etc. Most of these are
43 normally in the top commit so it is easy to change them with 'git 43 normally in the top commit so it is easy to change them with 'git
44 commit --amend' 44 commit --amend'
45 - Each branch tracks the upstream branch, so that this script can 45 - Each branch tracks the upstream branch, so that this script can
46 automatically determine the number of commits in it (optional) 46 automatically determine the number of commits in it (optional)
47 - Check out a branch, and run this script to create and send out your 47 - Check out a branch, and run this script to create and send out your
48 patches. Weeks later, change the patches and repeat, knowing that you 48 patches. Weeks later, change the patches and repeat, knowing that you
49 will get a consistent result each time. 49 will get a consistent result each time.
50 50
51 51
52 How to configure it 52 How to configure it
53 =================== 53 ===================
54 54
55 For most cases of using patman for U-Boot development, patman will 55 For most cases of using patman for U-Boot development, patman will
56 locate and use the file 'doc/git-mailrc' in your U-Boot directory. 56 locate and use the file 'doc/git-mailrc' in your U-Boot directory.
57 This contains most of the aliases you will need. 57 This contains most of the aliases you will need.
58 58
59 For Linux the 'scripts/get_maintainer.pl' handles figuring out where 59 For Linux the 'scripts/get_maintainer.pl' handles figuring out where
60 to send patches pretty well. 60 to send patches pretty well.
61 61
62 During the first run patman creates a config file for you by taking the default 62 During the first run patman creates a config file for you by taking the default
63 user name and email address from the global .gitconfig file. 63 user name and email address from the global .gitconfig file.
64 64
65 To add your own, create a file ~/.patman like this: 65 To add your own, create a file ~/.patman like this:
66 66
67 >>>> 67 >>>>
68 # patman alias file 68 # patman alias file
69 69
70 [alias] 70 [alias]
71 me: Simon Glass <sjg@chromium.org> 71 me: Simon Glass <sjg@chromium.org>
72 72
73 u-boot: U-Boot Mailing List <u-boot@lists.denx.de> 73 u-boot: U-Boot Mailing List <u-boot@lists.denx.de>
74 wolfgang: Wolfgang Denk <wd@denx.de> 74 wolfgang: Wolfgang Denk <wd@denx.de>
75 others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net> 75 others: Mike Frysinger <vapier@gentoo.org>, Fred Bloggs <f.bloggs@napier.net>
76 76
77 <<<< 77 <<<<
78 78
79 Aliases are recursive. 79 Aliases are recursive.
80 80
81 The checkpatch.pl in the U-Boot tools/ subdirectory will be located and 81 The checkpatch.pl in the U-Boot tools/ subdirectory will be located and
82 used. Failing that you can put it into your path or ~/bin/checkpatch.pl 82 used. Failing that you can put it into your path or ~/bin/checkpatch.pl
83 83
84 84
85 If you want to change the defaults for patman's command-line arguments, 85 If you want to change the defaults for patman's command-line arguments,
86 you can add a [settings] section to your .patman file. This can be used 86 you can add a [settings] section to your .patman file. This can be used
87 for any command line option by referring to the "dest" for the option in 87 for any command line option by referring to the "dest" for the option in
88 patman.py. For reference, the useful ones (at the moment) shown below 88 patman.py. For reference, the useful ones (at the moment) shown below
89 (all with the non-default setting): 89 (all with the non-default setting):
90 90
91 >>> 91 >>>
92 92
93 [settings] 93 [settings]
94 ignore_errors: True 94 ignore_errors: True
95 process_tags: False 95 process_tags: False
96 verbose: True 96 verbose: True
97 97
98 <<< 98 <<<
99 99
100 100
101 If you want to adjust settings (or aliases) that affect just a single 101 If you want to adjust settings (or aliases) that affect just a single
102 project you can add a section that looks like [project_settings] or 102 project you can add a section that looks like [project_settings] or
103 [project_alias]. If you want to use tags for your linux work, you could 103 [project_alias]. If you want to use tags for your linux work, you could
104 do: 104 do:
105 105
106 >>> 106 >>>
107 107
108 [linux_settings] 108 [linux_settings]
109 process_tags: True 109 process_tags: True
110 110
111 <<< 111 <<<
112 112
113 113
114 How to run it 114 How to run it
115 ============= 115 =============
116 116
117 First do a dry run: 117 First do a dry run:
118 118
119 $ ./tools/patman/patman -n 119 $ ./tools/patman/patman -n
120 120
121 If it can't detect the upstream branch, try telling it how many patches 121 If it can't detect the upstream branch, try telling it how many patches
122 there are in your series: 122 there are in your series:
123 123
124 $ ./tools/patman/patman -n -c5 124 $ ./tools/patman/patman -n -c5
125 125
126 This will create patch files in your current directory and tell you who 126 This will create patch files in your current directory and tell you who
127 it is thinking of sending them to. Take a look at the patch files. 127 it is thinking of sending them to. Take a look at the patch files.
128 128
129 $ ./tools/patman/patman -n -c5 -s1 129 $ ./tools/patman/patman -n -c5 -s1
130 130
131 Similar to the above, but skip the first commit and take the next 5. This 131 Similar to the above, but skip the first commit and take the next 5. This
132 is useful if your top commit is for setting up testing. 132 is useful if your top commit is for setting up testing.
133 133
134 134
135 How to add tags 135 How to add tags
136 =============== 136 ===============
137 137
138 To make this script useful you must add tags like the following into any 138 To make this script useful you must add tags like the following into any
139 commit. Most can only appear once in the whole series. 139 commit. Most can only appear once in the whole series.
140 140
141 Series-to: email / alias 141 Series-to: email / alias
142 Email address / alias to send patch series to (you can add this 142 Email address / alias to send patch series to (you can add this
143 multiple times) 143 multiple times)
144 144
145 Series-cc: email / alias, ... 145 Series-cc: email / alias, ...
146 Email address / alias to Cc patch series to (you can add this 146 Email address / alias to Cc patch series to (you can add this
147 multiple times) 147 multiple times)
148 148
149 Series-version: n 149 Series-version: n
150 Sets the version number of this patch series 150 Sets the version number of this patch series
151 151
152 Series-prefix: prefix 152 Series-prefix: prefix
153 Sets the subject prefix. Normally empty but it can be RFC for 153 Sets the subject prefix. Normally empty but it can be RFC for
154 RFC patches, or RESEND if you are being ignored. 154 RFC patches, or RESEND if you are being ignored.
155 155
156 Series-name: name 156 Series-name: name
157 Sets the name of the series. You don't need to have a name, and 157 Sets the name of the series. You don't need to have a name, and
158 patman does not yet use it, but it is convenient to put the branch 158 patman does not yet use it, but it is convenient to put the branch
159 name here to help you keep track of multiple upstreaming efforts. 159 name here to help you keep track of multiple upstreaming efforts.
160 160
161 Cover-letter: 161 Cover-letter:
162 This is the patch set title 162 This is the patch set title
163 blah blah 163 blah blah
164 more blah blah 164 more blah blah
165 END 165 END
166 Sets the cover letter contents for the series. The first line 166 Sets the cover letter contents for the series. The first line
167 will become the subject of the cover letter 167 will become the subject of the cover letter
168 168
169 Cover-letter-cc: email / alias 169 Cover-letter-cc: email / alias
170 Additional email addresses / aliases to send cover letter to (you 170 Additional email addresses / aliases to send cover letter to (you
171 can add this multiple times) 171 can add this multiple times)
172 172
173 Series-notes: 173 Series-notes:
174 blah blah 174 blah blah
175 blah blah 175 blah blah
176 more blah blah 176 more blah blah
177 END 177 END
178 Sets some notes for the patch series, which you don't want in 178 Sets some notes for the patch series, which you don't want in
179 the commit messages, but do want to send, The notes are joined 179 the commit messages, but do want to send, The notes are joined
180 together and put after the cover letter. Can appear multiple 180 together and put after the cover letter. Can appear multiple
181 times. 181 times.
182 182
183 Commit-notes: 183 Commit-notes:
184 blah blah 184 blah blah
185 blah blah 185 blah blah
186 more blah blah 186 more blah blah
187 END 187 END
188 Similar, but for a single commit (patch). These notes will appear 188 Similar, but for a single commit (patch). These notes will appear
189 immediately below the --- cut in the patch file. 189 immediately below the --- cut in the patch file.
190 190
191 Signed-off-by: Their Name <email> 191 Signed-off-by: Their Name <email>
192 A sign-off is added automatically to your patches (this is 192 A sign-off is added automatically to your patches (this is
193 probably a bug). If you put this tag in your patches, it will 193 probably a bug). If you put this tag in your patches, it will
194 override the default signoff that patman automatically adds. 194 override the default signoff that patman automatically adds.
195 Multiple duplicate signoffs will be removed.
195 196
196 Tested-by: Their Name <email> 197 Tested-by: Their Name <email>
197 Reviewed-by: Their Name <email> 198 Reviewed-by: Their Name <email>
198 Acked-by: Their Name <email> 199 Acked-by: Their Name <email>
199 These indicate that someone has tested/reviewed/acked your patch. 200 These indicate that someone has tested/reviewed/acked your patch.
200 When you get this reply on the mailing list, you can add this 201 When you get this reply on the mailing list, you can add this
201 tag to the relevant commit and the script will include it when 202 tag to the relevant commit and the script will include it when
202 you send out the next version. If 'Tested-by:' is set to 203 you send out the next version. If 'Tested-by:' is set to
203 yourself, it will be removed. No one will believe you. 204 yourself, it will be removed. No one will believe you.
204 205
205 Series-changes: n 206 Series-changes: n
206 - Guinea pig moved into its cage 207 - Guinea pig moved into its cage
207 - Other changes ending with a blank line 208 - Other changes ending with a blank line
208 <blank line> 209 <blank line>
209 This can appear in any commit. It lists the changes for a 210 This can appear in any commit. It lists the changes for a
210 particular version n of that commit. The change list is 211 particular version n of that commit. The change list is
211 created based on this information. Each commit gets its own 212 created based on this information. Each commit gets its own
212 change list and also the whole thing is repeated in the cover 213 change list and also the whole thing is repeated in the cover
213 letter (where duplicate change lines are merged). 214 letter (where duplicate change lines are merged).
214 215
215 By adding your change lists into your commits it is easier to 216 By adding your change lists into your commits it is easier to
216 keep track of what happened. When you amend a commit, remember 217 keep track of what happened. When you amend a commit, remember
217 to update the log there and then, knowing that the script will 218 to update the log there and then, knowing that the script will
218 do the rest. 219 do the rest.
219 220
220 Patch-cc: Their Name <email> 221 Patch-cc: Their Name <email>
221 This copies a single patch to another email address. Note that the 222 This copies a single patch to another email address. Note that the
222 Cc: used by git send-email is ignored by patman, but will be 223 Cc: used by git send-email is ignored by patman, but will be
223 interpreted by git send-email if you use it. 224 interpreted by git send-email if you use it.
224 225
225 Series-process-log: sort, uniq 226 Series-process-log: sort, uniq
226 This tells patman to sort and/or uniq the change logs. It is 227 This tells patman to sort and/or uniq the change logs. It is
227 assumed that each change log entry is only a single line long. 228 assumed that each change log entry is only a single line long.
228 Use 'sort' to sort the entries, and 'uniq' to include only 229 Use 'sort' to sort the entries, and 'uniq' to include only
229 unique entries. If omitted, no change log processing is done. 230 unique entries. If omitted, no change log processing is done.
230 Separate each tag with a comma. 231 Separate each tag with a comma.
231 232
232 Various other tags are silently removed, like these Chrome OS and 233 Various other tags are silently removed, like these Chrome OS and
233 Gerrit tags: 234 Gerrit tags:
234 235
235 BUG=... 236 BUG=...
236 TEST=... 237 TEST=...
237 Change-Id: 238 Change-Id:
238 Review URL: 239 Review URL:
239 Reviewed-on: 240 Reviewed-on:
240 Commit-xxxx: (except Commit-notes) 241 Commit-xxxx: (except Commit-notes)
241 242
242 Exercise for the reader: Try adding some tags to one of your current 243 Exercise for the reader: Try adding some tags to one of your current
243 patch series and see how the patches turn out. 244 patch series and see how the patches turn out.
244 245
245 246
246 Where Patches Are Sent 247 Where Patches Are Sent
247 ====================== 248 ======================
248 249
249 Once the patches are created, patman sends them using git send-email. The 250 Once the patches are created, patman sends them using git send-email. The
250 whole series is sent to the recipients in Series-to: and Series-cc. 251 whole series is sent to the recipients in Series-to: and Series-cc.
251 You can Cc individual patches to other people with the Patch-cc: tag. Tags 252 You can Cc individual patches to other people with the Patch-cc: tag. Tags
252 in the subject are also picked up to Cc patches. For example, a commit like 253 in the subject are also picked up to Cc patches. For example, a commit like
253 this: 254 this:
254 255
255 >>>> 256 >>>>
256 commit 10212537b85ff9b6e09c82045127522c0f0db981 257 commit 10212537b85ff9b6e09c82045127522c0f0db981
257 Author: Mike Frysinger <vapier@gentoo.org> 258 Author: Mike Frysinger <vapier@gentoo.org>
258 Date: Mon Nov 7 23:18:44 2011 -0500 259 Date: Mon Nov 7 23:18:44 2011 -0500
259 260
260 x86: arm: add a git mailrc file for maintainers 261 x86: arm: add a git mailrc file for maintainers
261 262
262 This should make sending out e-mails to the right people easier. 263 This should make sending out e-mails to the right people easier.
263 264
264 Patch-cc: sandbox, mikef, ag 265 Patch-cc: sandbox, mikef, ag
265 Patch-cc: afleming 266 Patch-cc: afleming
266 <<<< 267 <<<<
267 268
268 will create a patch which is copied to x86, arm, sandbox, mikef, ag and 269 will create a patch which is copied to x86, arm, sandbox, mikef, ag and
269 afleming. 270 afleming.
270 271
271 If you have a cover letter it will get sent to the union of the Patch-cc 272 If you have a cover letter it will get sent to the union of the Patch-cc
272 lists of all of the other patches. If you want to sent it to additional 273 lists of all of the other patches. If you want to sent it to additional
273 people you can add a tag: 274 people you can add a tag:
274 275
275 Cover-letter-cc: <list of addresses> 276 Cover-letter-cc: <list of addresses>
276 277
277 These people will get the cover letter even if they are not on the To/Cc 278 These people will get the cover letter even if they are not on the To/Cc
278 list for any of the patches. 279 list for any of the patches.
279 280
280 281
281 Example Work Flow 282 Example Work Flow
282 ================= 283 =================
283 284
284 The basic workflow is to create your commits, add some tags to the top 285 The basic workflow is to create your commits, add some tags to the top
285 commit, and type 'patman' to check and send them. 286 commit, and type 'patman' to check and send them.
286 287
287 Here is an example workflow for a series of 4 patches. Let's say you have 288 Here is an example workflow for a series of 4 patches. Let's say you have
288 these rather contrived patches in the following order in branch us-cmd in 289 these rather contrived patches in the following order in branch us-cmd in
289 your tree where 'us' means your upstreaming activity (newest to oldest as 290 your tree where 'us' means your upstreaming activity (newest to oldest as
290 output by git log --oneline): 291 output by git log --oneline):
291 292
292 7c7909c wip 293 7c7909c wip
293 89234f5 Don't include standard parser if hush is used 294 89234f5 Don't include standard parser if hush is used
294 8d640a7 mmc: sparc: Stop using builtin_run_command() 295 8d640a7 mmc: sparc: Stop using builtin_run_command()
295 0c859a9 Rename run_command2() to run_command() 296 0c859a9 Rename run_command2() to run_command()
296 a74443f sandbox: Rename run_command() to builtin_run_command() 297 a74443f sandbox: Rename run_command() to builtin_run_command()
297 298
298 The first patch is some test things that enable your code to be compiled, 299 The first patch is some test things that enable your code to be compiled,
299 but that you don't want to submit because there is an existing patch for it 300 but that you don't want to submit because there is an existing patch for it
300 on the list. So you can tell patman to create and check some patches 301 on the list. So you can tell patman to create and check some patches
301 (skipping the first patch) with: 302 (skipping the first patch) with:
302 303
303 patman -s1 -n 304 patman -s1 -n
304 305
305 If you want to do all of them including the work-in-progress one, then 306 If you want to do all of them including the work-in-progress one, then
306 (if you are tracking an upstream branch): 307 (if you are tracking an upstream branch):
307 308
308 patman -n 309 patman -n
309 310
310 Let's say that patman reports an error in the second patch. Then: 311 Let's say that patman reports an error in the second patch. Then:
311 312
312 git rebase -i HEAD~6 313 git rebase -i HEAD~6
313 <change 'pick' to 'edit' in 89234f5> 314 <change 'pick' to 'edit' in 89234f5>
314 <use editor to make code changes> 315 <use editor to make code changes>
315 git add -u 316 git add -u
316 git rebase --continue 317 git rebase --continue
317 318
318 Now you have an updated patch series. To check it: 319 Now you have an updated patch series. To check it:
319 320
320 patman -s1 -n 321 patman -s1 -n
321 322
322 Let's say it is now clean and you want to send it. Now you need to set up 323 Let's say it is now clean and you want to send it. Now you need to set up
323 the destination. So amend the top commit with: 324 the destination. So amend the top commit with:
324 325
325 git commit --amend 326 git commit --amend
326 327
327 Use your editor to add some tags, so that the whole commit message is: 328 Use your editor to add some tags, so that the whole commit message is:
328 329
329 The current run_command() is really only one of the options, with 330 The current run_command() is really only one of the options, with
330 hush providing the other. It really shouldn't be called directly 331 hush providing the other. It really shouldn't be called directly
331 in case the hush parser is bring used, so rename this function to 332 in case the hush parser is bring used, so rename this function to
332 better explain its purpose. 333 better explain its purpose.
333 334
334 Series-to: u-boot 335 Series-to: u-boot
335 Series-cc: bfin, marex 336 Series-cc: bfin, marex
336 Series-prefix: RFC 337 Series-prefix: RFC
337 Cover-letter: 338 Cover-letter:
338 Unified command execution in one place 339 Unified command execution in one place
339 340
340 At present two parsers have similar code to execute commands. Also 341 At present two parsers have similar code to execute commands. Also
341 cmd_usage() is called all over the place. This series adds a single 342 cmd_usage() is called all over the place. This series adds a single
342 function which processes commands called cmd_process(). 343 function which processes commands called cmd_process().
343 END 344 END
344 345
345 Change-Id: Ica71a14c1f0ecb5650f771a32fecb8d2eb9d8a17 346 Change-Id: Ica71a14c1f0ecb5650f771a32fecb8d2eb9d8a17
346 347
347 348
348 You want this to be an RFC and Cc the whole series to the bfin alias and 349 You want this to be an RFC and Cc the whole series to the bfin alias and
349 to Marek. Two of the patches have tags (those are the bits at the front of 350 to Marek. Two of the patches have tags (those are the bits at the front of
350 the subject that say mmc: sparc: and sandbox:), so 8d640a7 will be Cc'd to 351 the subject that say mmc: sparc: and sandbox:), so 8d640a7 will be Cc'd to
351 mmc and sparc, and the last one to sandbox. 352 mmc and sparc, and the last one to sandbox.
352 353
353 Now to send the patches, take off the -n flag: 354 Now to send the patches, take off the -n flag:
354 355
355 patman -s1 356 patman -s1
356 357
357 The patches will be created, shown in your editor, and then sent along with 358 The patches will be created, shown in your editor, and then sent along with
358 the cover letter. Note that patman's tags are automatically removed so that 359 the cover letter. Note that patman's tags are automatically removed so that
359 people on the list don't see your secret info. 360 people on the list don't see your secret info.
360 361
361 Of course patches often attract comments and you need to make some updates. 362 Of course patches often attract comments and you need to make some updates.
362 Let's say one person sent comments and you get an Acked-by: on one patch. 363 Let's say one person sent comments and you get an Acked-by: on one patch.
363 Also, the patch on the list that you were waiting for has been merged, 364 Also, the patch on the list that you were waiting for has been merged,
364 so you can drop your wip commit. So you resync with upstream: 365 so you can drop your wip commit. So you resync with upstream:
365 366
366 git fetch origin (or whatever upstream is called) 367 git fetch origin (or whatever upstream is called)
367 git rebase origin/master 368 git rebase origin/master
368 369
369 and use git rebase -i to edit the commits, dropping the wip one. You add 370 and use git rebase -i to edit the commits, dropping the wip one. You add
370 the ack tag to one commit: 371 the ack tag to one commit:
371 372
372 Acked-by: Heiko Schocher <hs@denx.de> 373 Acked-by: Heiko Schocher <hs@denx.de>
373 374
374 update the Series-cc: in the top commit: 375 update the Series-cc: in the top commit:
375 376
376 Series-cc: bfin, marex, Heiko Schocher <hs@denx.de> 377 Series-cc: bfin, marex, Heiko Schocher <hs@denx.de>
377 378
378 and remove the Series-prefix: tag since it it isn't an RFC any more. The 379 and remove the Series-prefix: tag since it it isn't an RFC any more. The
379 series is now version two, so the series info in the top commit looks like 380 series is now version two, so the series info in the top commit looks like
380 this: 381 this:
381 382
382 Series-to: u-boot 383 Series-to: u-boot
383 Series-cc: bfin, marex, Heiko Schocher <hs@denx.de> 384 Series-cc: bfin, marex, Heiko Schocher <hs@denx.de>
384 Series-version: 2 385 Series-version: 2
385 Cover-letter: 386 Cover-letter:
386 ... 387 ...
387 388
388 Finally, you need to add a change log to the two commits you changed. You 389 Finally, you need to add a change log to the two commits you changed. You
389 add change logs to each individual commit where the changes happened, like 390 add change logs to each individual commit where the changes happened, like
390 this: 391 this:
391 392
392 Series-changes: 2 393 Series-changes: 2
393 - Updated the command decoder to reduce code size 394 - Updated the command decoder to reduce code size
394 - Wound the torque propounder up a little more 395 - Wound the torque propounder up a little more
395 396
396 (note the blank line at the end of the list) 397 (note the blank line at the end of the list)
397 398
398 When you run patman it will collect all the change logs from the different 399 When you run patman it will collect all the change logs from the different
399 commits and combine them into the cover letter, if you have one. So finally 400 commits and combine them into the cover letter, if you have one. So finally
400 you have a new series of commits: 401 you have a new series of commits:
401 402
402 faeb973 Don't include standard parser if hush is used 403 faeb973 Don't include standard parser if hush is used
403 1b2f2fe mmc: sparc: Stop using builtin_run_command() 404 1b2f2fe mmc: sparc: Stop using builtin_run_command()
404 cfbe330 Rename run_command2() to run_command() 405 cfbe330 Rename run_command2() to run_command()
405 0682677 sandbox: Rename run_command() to builtin_run_command() 406 0682677 sandbox: Rename run_command() to builtin_run_command()
406 407
407 so to send them: 408 so to send them:
408 409
409 patman 410 patman
410 411
411 and it will create and send the version 2 series. 412 and it will create and send the version 2 series.
412 413
413 General points: 414 General points:
414 415
415 1. When you change back to the us-cmd branch days or weeks later all your 416 1. When you change back to the us-cmd branch days or weeks later all your
416 information is still there, safely stored in the commits. You don't need 417 information is still there, safely stored in the commits. You don't need
417 to remember what version you are up to, who you sent the last lot of patches 418 to remember what version you are up to, who you sent the last lot of patches
418 to, or anything about the change logs. 419 to, or anything about the change logs.
419 420
420 2. If you put tags in the subject, patman will Cc the maintainers 421 2. If you put tags in the subject, patman will Cc the maintainers
421 automatically in many cases. 422 automatically in many cases.
422 423
423 3. If you want to keep the commits from each series you sent so that you can 424 3. If you want to keep the commits from each series you sent so that you can
424 compare change and see what you did, you can either create a new branch for 425 compare change and see what you did, you can either create a new branch for
425 each version, or just tag the branch before you start changing it: 426 each version, or just tag the branch before you start changing it:
426 427
427 git tag sent/us-cmd-rfc 428 git tag sent/us-cmd-rfc
428 ...later... 429 ...later...
429 git tag sent/us-cmd-v2 430 git tag sent/us-cmd-v2
430 431
431 4. If you want to modify the patches a little before sending, you can do 432 4. If you want to modify the patches a little before sending, you can do
432 this in your editor, but be careful! 433 this in your editor, but be careful!
433 434
434 5. If you want to run git send-email yourself, use the -n flag which will 435 5. If you want to run git send-email yourself, use the -n flag which will
435 print out the command line patman would have used. 436 print out the command line patman would have used.
436 437
437 6. It is a good idea to add the change log info as you change the commit, 438 6. It is a good idea to add the change log info as you change the commit,
438 not later when you can't remember which patch you changed. You can always 439 not later when you can't remember which patch you changed. You can always
439 go back and change or remove logs from commits. 440 go back and change or remove logs from commits.
440 441
441 442
442 Other thoughts 443 Other thoughts
443 ============== 444 ==============
444 445
445 This script has been split into sensible files but still needs work. 446 This script has been split into sensible files but still needs work.
446 Most of these are indicated by a TODO in the code. 447 Most of these are indicated by a TODO in the code.
447 448
448 It would be nice if this could handle the In-reply-to side of things. 449 It would be nice if this could handle the In-reply-to side of things.
449 450
450 The tests are incomplete, as is customary. Use the --test flag to run them, 451 The tests are incomplete, as is customary. Use the --test flag to run them,
451 and make sure you are in the tools/patman directory first: 452 and make sure you are in the tools/patman directory first:
452 453
453 $ cd /path/to/u-boot 454 $ cd /path/to/u-boot
454 $ cd tools/patman 455 $ cd tools/patman
455 $ ./patman --test 456 $ ./patman --test
456 457
457 Error handling doesn't always produce friendly error messages - e.g. 458 Error handling doesn't always produce friendly error messages - e.g.
458 putting an incorrect tag in a commit may provide a confusing message. 459 putting an incorrect tag in a commit may provide a confusing message.
459 460
460 There might be a few other features not mentioned in this README. They 461 There might be a few other features not mentioned in this README. They
461 might be bugs. In particular, tags are case sensitive which is probably 462 might be bugs. In particular, tags are case sensitive which is probably
462 a bad thing. 463 a bad thing.
463 464
464 465
465 Simon Glass <sjg@chromium.org> 466 Simon Glass <sjg@chromium.org>
466 v1, v2, 19-Oct-11 467 v1, v2, 19-Oct-11
467 revised v3 24-Nov-11 468 revised v3 24-Nov-11
468 469
tools/patman/commit.py
1 # Copyright (c) 2011 The Chromium OS Authors. 1 # Copyright (c) 2011 The Chromium OS Authors.
2 # 2 #
3 # SPDX-License-Identifier: GPL-2.0+ 3 # SPDX-License-Identifier: GPL-2.0+
4 # 4 #
5 5
6 import re 6 import re
7 7
8 # Separates a tag: at the beginning of the subject from the rest of it 8 # Separates a tag: at the beginning of the subject from the rest of it
9 re_subject_tag = re.compile('([^:\s]*):\s*(.*)') 9 re_subject_tag = re.compile('([^:\s]*):\s*(.*)')
10 10
11 class Commit: 11 class Commit:
12 """Holds information about a single commit/patch in the series. 12 """Holds information about a single commit/patch in the series.
13 13
14 Args: 14 Args:
15 hash: Commit hash (as a string) 15 hash: Commit hash (as a string)
16 16
17 Variables: 17 Variables:
18 hash: Commit hash 18 hash: Commit hash
19 subject: Subject line 19 subject: Subject line
20 tags: List of maintainer tag strings 20 tags: List of maintainer tag strings
21 changes: Dict containing a list of changes (single line strings). 21 changes: Dict containing a list of changes (single line strings).
22 The dict is indexed by change version (an integer) 22 The dict is indexed by change version (an integer)
23 cc_list: List of people to aliases/emails to cc on this commit 23 cc_list: List of people to aliases/emails to cc on this commit
24 notes: List of lines in the commit (not series) notes 24 notes: List of lines in the commit (not series) notes
25 """ 25 """
26 def __init__(self, hash): 26 def __init__(self, hash):
27 self.hash = hash 27 self.hash = hash
28 self.subject = None 28 self.subject = None
29 self.tags = [] 29 self.tags = []
30 self.changes = {} 30 self.changes = {}
31 self.cc_list = [] 31 self.cc_list = []
32 self.signoff_set = set()
32 self.notes = [] 33 self.notes = []
33 34
34 def AddChange(self, version, info): 35 def AddChange(self, version, info):
35 """Add a new change line to the change list for a version. 36 """Add a new change line to the change list for a version.
36 37
37 Args: 38 Args:
38 version: Patch set version (integer: 1, 2, 3) 39 version: Patch set version (integer: 1, 2, 3)
39 info: Description of change in this version 40 info: Description of change in this version
40 """ 41 """
41 if not self.changes.get(version): 42 if not self.changes.get(version):
42 self.changes[version] = [] 43 self.changes[version] = []
43 self.changes[version].append(info) 44 self.changes[version].append(info)
44 45
45 def CheckTags(self): 46 def CheckTags(self):
46 """Create a list of subject tags in the commit 47 """Create a list of subject tags in the commit
47 48
48 Subject tags look like this: 49 Subject tags look like this:
49 50
50 propounder: fort: Change the widget to propound correctly 51 propounder: fort: Change the widget to propound correctly
51 52
52 Here the tags are propounder and fort. Multiple tags are supported. 53 Here the tags are propounder and fort. Multiple tags are supported.
53 The list is updated in self.tag. 54 The list is updated in self.tag.
54 55
55 Returns: 56 Returns:
56 None if ok, else the name of a tag with no email alias 57 None if ok, else the name of a tag with no email alias
57 """ 58 """
58 str = self.subject 59 str = self.subject
59 m = True 60 m = True
60 while m: 61 while m:
61 m = re_subject_tag.match(str) 62 m = re_subject_tag.match(str)
62 if m: 63 if m:
63 tag = m.group(1) 64 tag = m.group(1)
64 self.tags.append(tag) 65 self.tags.append(tag)
65 str = m.group(2) 66 str = m.group(2)
66 return None 67 return None
67 68
68 def AddCc(self, cc_list): 69 def AddCc(self, cc_list):
69 """Add a list of people to Cc when we send this patch. 70 """Add a list of people to Cc when we send this patch.
70 71
71 Args: 72 Args:
72 cc_list: List of aliases or email addresses 73 cc_list: List of aliases or email addresses
73 """ 74 """
74 self.cc_list += cc_list 75 self.cc_list += cc_list
76
77 def CheckDuplicateSignoff(self, signoff):
78 """Check a list of signoffs we have send for this patch
79
80 Args:
81 signoff: Signoff line
82 Returns:
83 True if this signoff is new, False if we have already seen it.
84 """
85 if signoff in self.signoff_set:
86 return False
87 self.signoff_set.add(signoff)
88 return True
75 89
tools/patman/patchstream.py
1 # Copyright (c) 2011 The Chromium OS Authors. 1 # Copyright (c) 2011 The Chromium OS Authors.
2 # 2 #
3 # SPDX-License-Identifier: GPL-2.0+ 3 # SPDX-License-Identifier: GPL-2.0+
4 # 4 #
5 5
6 import os 6 import os
7 import re 7 import re
8 import shutil 8 import shutil
9 import tempfile 9 import tempfile
10 10
11 import command 11 import command
12 import commit 12 import commit
13 import gitutil 13 import gitutil
14 from series import Series 14 from series import Series
15 15
16 # Tags that we detect and remove 16 # Tags that we detect and remove
17 re_remove = re.compile('^BUG=|^TEST=|^BRANCH=|^Change-Id:|^Review URL:' 17 re_remove = re.compile('^BUG=|^TEST=|^BRANCH=|^Change-Id:|^Review URL:'
18 '|Reviewed-on:|Commit-\w*:') 18 '|Reviewed-on:|Commit-\w*:')
19 19
20 # Lines which are allowed after a TEST= line 20 # Lines which are allowed after a TEST= line
21 re_allowed_after_test = re.compile('^Signed-off-by:') 21 re_allowed_after_test = re.compile('^Signed-off-by:')
22 22
23 # Signoffs 23 # Signoffs
24 re_signoff = re.compile('^Signed-off-by:') 24 re_signoff = re.compile('^Signed-off-by: *(.*)')
25 25
26 # The start of the cover letter 26 # The start of the cover letter
27 re_cover = re.compile('^Cover-letter:') 27 re_cover = re.compile('^Cover-letter:')
28 28
29 # A cover letter Cc 29 # A cover letter Cc
30 re_cover_cc = re.compile('^Cover-letter-cc: *(.*)') 30 re_cover_cc = re.compile('^Cover-letter-cc: *(.*)')
31 31
32 # Patch series tag 32 # Patch series tag
33 re_series_tag = re.compile('^Series-([a-z-]*): *(.*)') 33 re_series_tag = re.compile('^Series-([a-z-]*): *(.*)')
34 34
35 # Commit series tag 35 # Commit series tag
36 re_commit_tag = re.compile('^Commit-([a-z-]*): *(.*)') 36 re_commit_tag = re.compile('^Commit-([a-z-]*): *(.*)')
37 37
38 # Commit tags that we want to collect and keep 38 # Commit tags that we want to collect and keep
39 re_tag = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc): (.*)') 39 re_tag = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc): (.*)')
40 40
41 # The start of a new commit in the git log 41 # The start of a new commit in the git log
42 re_commit = re.compile('^commit ([0-9a-f]*)$') 42 re_commit = re.compile('^commit ([0-9a-f]*)$')
43 43
44 # We detect these since checkpatch doesn't always do it 44 # We detect these since checkpatch doesn't always do it
45 re_space_before_tab = re.compile('^[+].* \t') 45 re_space_before_tab = re.compile('^[+].* \t')
46 46
47 # States we can be in - can we use range() and still have comments? 47 # States we can be in - can we use range() and still have comments?
48 STATE_MSG_HEADER = 0 # Still in the message header 48 STATE_MSG_HEADER = 0 # Still in the message header
49 STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit) 49 STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit)
50 STATE_PATCH_HEADER = 2 # In patch header (after the subject) 50 STATE_PATCH_HEADER = 2 # In patch header (after the subject)
51 STATE_DIFFS = 3 # In the diff part (past --- line) 51 STATE_DIFFS = 3 # In the diff part (past --- line)
52 52
53 class PatchStream: 53 class PatchStream:
54 """Class for detecting/injecting tags in a patch or series of patches 54 """Class for detecting/injecting tags in a patch or series of patches
55 55
56 We support processing the output of 'git log' to read out the tags we 56 We support processing the output of 'git log' to read out the tags we
57 are interested in. We can also process a patch file in order to remove 57 are interested in. We can also process a patch file in order to remove
58 unwanted tags or inject additional ones. These correspond to the two 58 unwanted tags or inject additional ones. These correspond to the two
59 phases of processing. 59 phases of processing.
60 """ 60 """
61 def __init__(self, series, name=None, is_log=False): 61 def __init__(self, series, name=None, is_log=False):
62 self.skip_blank = False # True to skip a single blank line 62 self.skip_blank = False # True to skip a single blank line
63 self.found_test = False # Found a TEST= line 63 self.found_test = False # Found a TEST= line
64 self.lines_after_test = 0 # MNumber of lines found after TEST= 64 self.lines_after_test = 0 # MNumber of lines found after TEST=
65 self.warn = [] # List of warnings we have collected 65 self.warn = [] # List of warnings we have collected
66 self.linenum = 1 # Output line number we are up to 66 self.linenum = 1 # Output line number we are up to
67 self.in_section = None # Name of start...END section we are in 67 self.in_section = None # Name of start...END section we are in
68 self.notes = [] # Series notes 68 self.notes = [] # Series notes
69 self.section = [] # The current section...END section 69 self.section = [] # The current section...END section
70 self.series = series # Info about the patch series 70 self.series = series # Info about the patch series
71 self.is_log = is_log # True if indent like git log 71 self.is_log = is_log # True if indent like git log
72 self.in_change = 0 # Non-zero if we are in a change list 72 self.in_change = 0 # Non-zero if we are in a change list
73 self.blank_count = 0 # Number of blank lines stored up 73 self.blank_count = 0 # Number of blank lines stored up
74 self.state = STATE_MSG_HEADER # What state are we in? 74 self.state = STATE_MSG_HEADER # What state are we in?
75 self.tags = [] # Tags collected, like Tested-by... 75 self.tags = [] # Tags collected, like Tested-by...
76 self.signoff = [] # Contents of signoff line 76 self.signoff = [] # Contents of signoff line
77 self.commit = None # Current commit 77 self.commit = None # Current commit
78 78
79 def AddToSeries(self, line, name, value): 79 def AddToSeries(self, line, name, value):
80 """Add a new Series-xxx tag. 80 """Add a new Series-xxx tag.
81 81
82 When a Series-xxx tag is detected, we come here to record it, if we 82 When a Series-xxx tag is detected, we come here to record it, if we
83 are scanning a 'git log'. 83 are scanning a 'git log'.
84 84
85 Args: 85 Args:
86 line: Source line containing tag (useful for debug/error messages) 86 line: Source line containing tag (useful for debug/error messages)
87 name: Tag name (part after 'Series-') 87 name: Tag name (part after 'Series-')
88 value: Tag value (part after 'Series-xxx: ') 88 value: Tag value (part after 'Series-xxx: ')
89 """ 89 """
90 if name == 'notes': 90 if name == 'notes':
91 self.in_section = name 91 self.in_section = name
92 self.skip_blank = False 92 self.skip_blank = False
93 if self.is_log: 93 if self.is_log:
94 self.series.AddTag(self.commit, line, name, value) 94 self.series.AddTag(self.commit, line, name, value)
95 95
96 def AddToCommit(self, line, name, value): 96 def AddToCommit(self, line, name, value):
97 """Add a new Commit-xxx tag. 97 """Add a new Commit-xxx tag.
98 98
99 When a Commit-xxx tag is detected, we come here to record it. 99 When a Commit-xxx tag is detected, we come here to record it.
100 100
101 Args: 101 Args:
102 line: Source line containing tag (useful for debug/error messages) 102 line: Source line containing tag (useful for debug/error messages)
103 name: Tag name (part after 'Commit-') 103 name: Tag name (part after 'Commit-')
104 value: Tag value (part after 'Commit-xxx: ') 104 value: Tag value (part after 'Commit-xxx: ')
105 """ 105 """
106 if name == 'notes': 106 if name == 'notes':
107 self.in_section = 'commit-' + name 107 self.in_section = 'commit-' + name
108 self.skip_blank = False 108 self.skip_blank = False
109 109
110 def CloseCommit(self): 110 def CloseCommit(self):
111 """Save the current commit into our commit list, and reset our state""" 111 """Save the current commit into our commit list, and reset our state"""
112 if self.commit and self.is_log: 112 if self.commit and self.is_log:
113 self.series.AddCommit(self.commit) 113 self.series.AddCommit(self.commit)
114 self.commit = None 114 self.commit = None
115 115
116 def FormatTags(self, tags): 116 def FormatTags(self, tags):
117 out_list = [] 117 out_list = []
118 for tag in sorted(tags): 118 for tag in sorted(tags):
119 if tag.startswith('Cc:'): 119 if tag.startswith('Cc:'):
120 tag_list = tag[4:].split(',') 120 tag_list = tag[4:].split(',')
121 out_list += gitutil.BuildEmailList(tag_list, 'Cc:') 121 out_list += gitutil.BuildEmailList(tag_list, 'Cc:')
122 else: 122 else:
123 out_list.append(tag) 123 out_list.append(tag)
124 return out_list 124 return out_list
125 125
126 def ProcessLine(self, line): 126 def ProcessLine(self, line):
127 """Process a single line of a patch file or commit log 127 """Process a single line of a patch file or commit log
128 128
129 This process a line and returns a list of lines to output. The list 129 This process a line and returns a list of lines to output. The list
130 may be empty or may contain multiple output lines. 130 may be empty or may contain multiple output lines.
131 131
132 This is where all the complicated logic is located. The class's 132 This is where all the complicated logic is located. The class's
133 state is used to move between different states and detect things 133 state is used to move between different states and detect things
134 properly. 134 properly.
135 135
136 We can be in one of two modes: 136 We can be in one of two modes:
137 self.is_log == True: This is 'git log' mode, where most output is 137 self.is_log == True: This is 'git log' mode, where most output is
138 indented by 4 characters and we are scanning for tags 138 indented by 4 characters and we are scanning for tags
139 139
140 self.is_log == False: This is 'patch' mode, where we already have 140 self.is_log == False: This is 'patch' mode, where we already have
141 all the tags, and are processing patches to remove junk we 141 all the tags, and are processing patches to remove junk we
142 don't want, and add things we think are required. 142 don't want, and add things we think are required.
143 143
144 Args: 144 Args:
145 line: text line to process 145 line: text line to process
146 146
147 Returns: 147 Returns:
148 list of output lines, or [] if nothing should be output 148 list of output lines, or [] if nothing should be output
149 """ 149 """
150 # Initially we have no output. Prepare the input line string 150 # Initially we have no output. Prepare the input line string
151 out = [] 151 out = []
152 line = line.rstrip('\n') 152 line = line.rstrip('\n')
153 if self.is_log: 153 if self.is_log:
154 if line[:4] == ' ': 154 if line[:4] == ' ':
155 line = line[4:] 155 line = line[4:]
156 156
157 # Handle state transition and skipping blank lines 157 # Handle state transition and skipping blank lines
158 series_tag_match = re_series_tag.match(line) 158 series_tag_match = re_series_tag.match(line)
159 commit_tag_match = re_commit_tag.match(line) 159 commit_tag_match = re_commit_tag.match(line)
160 commit_match = re_commit.match(line) if self.is_log else None 160 commit_match = re_commit.match(line) if self.is_log else None
161 cover_cc_match = re_cover_cc.match(line) 161 cover_cc_match = re_cover_cc.match(line)
162 signoff_match = re_signoff.match(line)
162 tag_match = None 163 tag_match = None
163 if self.state == STATE_PATCH_HEADER: 164 if self.state == STATE_PATCH_HEADER:
164 tag_match = re_tag.match(line) 165 tag_match = re_tag.match(line)
165 is_blank = not line.strip() 166 is_blank = not line.strip()
166 if is_blank: 167 if is_blank:
167 if (self.state == STATE_MSG_HEADER 168 if (self.state == STATE_MSG_HEADER
168 or self.state == STATE_PATCH_SUBJECT): 169 or self.state == STATE_PATCH_SUBJECT):
169 self.state += 1 170 self.state += 1
170 171
171 # We don't have a subject in the text stream of patch files 172 # We don't have a subject in the text stream of patch files
172 # It has its own line with a Subject: tag 173 # It has its own line with a Subject: tag
173 if not self.is_log and self.state == STATE_PATCH_SUBJECT: 174 if not self.is_log and self.state == STATE_PATCH_SUBJECT:
174 self.state += 1 175 self.state += 1
175 elif commit_match: 176 elif commit_match:
176 self.state = STATE_MSG_HEADER 177 self.state = STATE_MSG_HEADER
177 178
178 # If we are in a section, keep collecting lines until we see END 179 # If we are in a section, keep collecting lines until we see END
179 if self.in_section: 180 if self.in_section:
180 if line == 'END': 181 if line == 'END':
181 if self.in_section == 'cover': 182 if self.in_section == 'cover':
182 self.series.cover = self.section 183 self.series.cover = self.section
183 elif self.in_section == 'notes': 184 elif self.in_section == 'notes':
184 if self.is_log: 185 if self.is_log:
185 self.series.notes += self.section 186 self.series.notes += self.section
186 elif self.in_section == 'commit-notes': 187 elif self.in_section == 'commit-notes':
187 if self.is_log: 188 if self.is_log:
188 self.commit.notes += self.section 189 self.commit.notes += self.section
189 else: 190 else:
190 self.warn.append("Unknown section '%s'" % self.in_section) 191 self.warn.append("Unknown section '%s'" % self.in_section)
191 self.in_section = None 192 self.in_section = None
192 self.skip_blank = True 193 self.skip_blank = True
193 self.section = [] 194 self.section = []
194 else: 195 else:
195 self.section.append(line) 196 self.section.append(line)
196 197
197 # Detect the commit subject 198 # Detect the commit subject
198 elif not is_blank and self.state == STATE_PATCH_SUBJECT: 199 elif not is_blank and self.state == STATE_PATCH_SUBJECT:
199 self.commit.subject = line 200 self.commit.subject = line
200 201
201 # Detect the tags we want to remove, and skip blank lines 202 # Detect the tags we want to remove, and skip blank lines
202 elif re_remove.match(line) and not commit_tag_match: 203 elif re_remove.match(line) and not commit_tag_match:
203 self.skip_blank = True 204 self.skip_blank = True
204 205
205 # TEST= should be the last thing in the commit, so remove 206 # TEST= should be the last thing in the commit, so remove
206 # everything after it 207 # everything after it
207 if line.startswith('TEST='): 208 if line.startswith('TEST='):
208 self.found_test = True 209 self.found_test = True
209 elif self.skip_blank and is_blank: 210 elif self.skip_blank and is_blank:
210 self.skip_blank = False 211 self.skip_blank = False
211 212
212 # Detect the start of a cover letter section 213 # Detect the start of a cover letter section
213 elif re_cover.match(line): 214 elif re_cover.match(line):
214 self.in_section = 'cover' 215 self.in_section = 'cover'
215 self.skip_blank = False 216 self.skip_blank = False
216 217
217 elif cover_cc_match: 218 elif cover_cc_match:
218 value = cover_cc_match.group(1) 219 value = cover_cc_match.group(1)
219 self.AddToSeries(line, 'cover-cc', value) 220 self.AddToSeries(line, 'cover-cc', value)
220 221
221 # If we are in a change list, key collected lines until a blank one 222 # If we are in a change list, key collected lines until a blank one
222 elif self.in_change: 223 elif self.in_change:
223 if is_blank: 224 if is_blank:
224 # Blank line ends this change list 225 # Blank line ends this change list
225 self.in_change = 0 226 self.in_change = 0
226 elif line == '---' or re_signoff.match(line): 227 elif line == '---':
227 self.in_change = 0 228 self.in_change = 0
228 out = self.ProcessLine(line) 229 out = self.ProcessLine(line)
229 else: 230 else:
230 if self.is_log: 231 if self.is_log:
231 self.series.AddChange(self.in_change, self.commit, line) 232 self.series.AddChange(self.in_change, self.commit, line)
232 self.skip_blank = False 233 self.skip_blank = False
233 234
234 # Detect Series-xxx tags 235 # Detect Series-xxx tags
235 elif series_tag_match: 236 elif series_tag_match:
236 name = series_tag_match.group(1) 237 name = series_tag_match.group(1)
237 value = series_tag_match.group(2) 238 value = series_tag_match.group(2)
238 if name == 'changes': 239 if name == 'changes':
239 # value is the version number: e.g. 1, or 2 240 # value is the version number: e.g. 1, or 2
240 try: 241 try:
241 value = int(value) 242 value = int(value)
242 except ValueError as str: 243 except ValueError as str:
243 raise ValueError("%s: Cannot decode version info '%s'" % 244 raise ValueError("%s: Cannot decode version info '%s'" %
244 (self.commit.hash, line)) 245 (self.commit.hash, line))
245 self.in_change = int(value) 246 self.in_change = int(value)
246 else: 247 else:
247 self.AddToSeries(line, name, value) 248 self.AddToSeries(line, name, value)
248 self.skip_blank = True 249 self.skip_blank = True
249 250
250 # Detect Commit-xxx tags 251 # Detect Commit-xxx tags
251 elif commit_tag_match: 252 elif commit_tag_match:
252 name = commit_tag_match.group(1) 253 name = commit_tag_match.group(1)
253 value = commit_tag_match.group(2) 254 value = commit_tag_match.group(2)
254 if name == 'notes': 255 if name == 'notes':
255 self.AddToCommit(line, name, value) 256 self.AddToCommit(line, name, value)
256 self.skip_blank = True 257 self.skip_blank = True
257 258
258 # Detect the start of a new commit 259 # Detect the start of a new commit
259 elif commit_match: 260 elif commit_match:
260 self.CloseCommit() 261 self.CloseCommit()
261 # TODO: We should store the whole hash, and just display a subset 262 # TODO: We should store the whole hash, and just display a subset
262 self.commit = commit.Commit(commit_match.group(1)[:8]) 263 self.commit = commit.Commit(commit_match.group(1)[:8])
263 264
264 # Detect tags in the commit message 265 # Detect tags in the commit message
265 elif tag_match: 266 elif tag_match:
266 # Remove Tested-by self, since few will take much notice 267 # Remove Tested-by self, since few will take much notice
267 if (tag_match.group(1) == 'Tested-by' and 268 if (tag_match.group(1) == 'Tested-by' and
268 tag_match.group(2).find(os.getenv('USER') + '@') != -1): 269 tag_match.group(2).find(os.getenv('USER') + '@') != -1):
269 self.warn.append("Ignoring %s" % line) 270 self.warn.append("Ignoring %s" % line)
270 elif tag_match.group(1) == 'Patch-cc': 271 elif tag_match.group(1) == 'Patch-cc':
271 self.commit.AddCc(tag_match.group(2).split(',')) 272 self.commit.AddCc(tag_match.group(2).split(','))
272 else: 273 else:
273 self.tags.append(line); 274 self.tags.append(line);
275
276 # Suppress duplicate signoffs
277 elif signoff_match:
278 if self.commit.CheckDuplicateSignoff(signoff_match.group(1)):
279 out = [line]
274 280
275 # Well that means this is an ordinary line 281 # Well that means this is an ordinary line
276 else: 282 else:
277 pos = 1 283 pos = 1
278 # Look for ugly ASCII characters 284 # Look for ugly ASCII characters
279 for ch in line: 285 for ch in line:
280 # TODO: Would be nicer to report source filename and line 286 # TODO: Would be nicer to report source filename and line
281 if ord(ch) > 0x80: 287 if ord(ch) > 0x80:
282 self.warn.append("Line %d/%d ('%s') has funny ascii char" % 288 self.warn.append("Line %d/%d ('%s') has funny ascii char" %
283 (self.linenum, pos, line)) 289 (self.linenum, pos, line))
284 pos += 1 290 pos += 1
285 291
286 # Look for space before tab 292 # Look for space before tab
287 m = re_space_before_tab.match(line) 293 m = re_space_before_tab.match(line)
288 if m: 294 if m:
289 self.warn.append('Line %d/%d has space before tab' % 295 self.warn.append('Line %d/%d has space before tab' %
290 (self.linenum, m.start())) 296 (self.linenum, m.start()))
291 297
292 # OK, we have a valid non-blank line 298 # OK, we have a valid non-blank line
293 out = [line] 299 out = [line]
294 self.linenum += 1 300 self.linenum += 1
295 self.skip_blank = False 301 self.skip_blank = False
296 if self.state == STATE_DIFFS: 302 if self.state == STATE_DIFFS:
297 pass 303 pass
298 304
299 # If this is the start of the diffs section, emit our tags and 305 # If this is the start of the diffs section, emit our tags and
300 # change log 306 # change log
301 elif line == '---': 307 elif line == '---':
302 self.state = STATE_DIFFS 308 self.state = STATE_DIFFS
303 309
304 # Output the tags (signeoff first), then change list 310 # Output the tags (signeoff first), then change list
305 out = [] 311 out = []
306 log = self.series.MakeChangeLog(self.commit) 312 log = self.series.MakeChangeLog(self.commit)
307 out += self.FormatTags(self.tags) 313 out += self.FormatTags(self.tags)
308 out += [line] + self.commit.notes + [''] + log 314 out += [line] + self.commit.notes + [''] + log
309 elif self.found_test: 315 elif self.found_test:
310 if not re_allowed_after_test.match(line): 316 if not re_allowed_after_test.match(line):
311 self.lines_after_test += 1 317 self.lines_after_test += 1
312 318
313 return out 319 return out
314 320
315 def Finalize(self): 321 def Finalize(self):
316 """Close out processing of this patch stream""" 322 """Close out processing of this patch stream"""
317 self.CloseCommit() 323 self.CloseCommit()
318 if self.lines_after_test: 324 if self.lines_after_test:
319 self.warn.append('Found %d lines after TEST=' % 325 self.warn.append('Found %d lines after TEST=' %
320 self.lines_after_test) 326 self.lines_after_test)
321 327
322 def ProcessStream(self, infd, outfd): 328 def ProcessStream(self, infd, outfd):
323 """Copy a stream from infd to outfd, filtering out unwanting things. 329 """Copy a stream from infd to outfd, filtering out unwanting things.
324 330
325 This is used to process patch files one at a time. 331 This is used to process patch files one at a time.
326 332
327 Args: 333 Args:
328 infd: Input stream file object 334 infd: Input stream file object
329 outfd: Output stream file object 335 outfd: Output stream file object
330 """ 336 """
331 # Extract the filename from each diff, for nice warnings 337 # Extract the filename from each diff, for nice warnings
332 fname = None 338 fname = None
333 last_fname = None 339 last_fname = None
334 re_fname = re.compile('diff --git a/(.*) b/.*') 340 re_fname = re.compile('diff --git a/(.*) b/.*')
335 while True: 341 while True:
336 line = infd.readline() 342 line = infd.readline()
337 if not line: 343 if not line:
338 break 344 break
339 out = self.ProcessLine(line) 345 out = self.ProcessLine(line)
340 346
341 # Try to detect blank lines at EOF 347 # Try to detect blank lines at EOF
342 for line in out: 348 for line in out:
343 match = re_fname.match(line) 349 match = re_fname.match(line)
344 if match: 350 if match:
345 last_fname = fname 351 last_fname = fname
346 fname = match.group(1) 352 fname = match.group(1)
347 if line == '+': 353 if line == '+':
348 self.blank_count += 1 354 self.blank_count += 1
349 else: 355 else:
350 if self.blank_count and (line == '-- ' or match): 356 if self.blank_count and (line == '-- ' or match):
351 self.warn.append("Found possible blank line(s) at " 357 self.warn.append("Found possible blank line(s) at "
352 "end of file '%s'" % last_fname) 358 "end of file '%s'" % last_fname)
353 outfd.write('+\n' * self.blank_count) 359 outfd.write('+\n' * self.blank_count)
354 outfd.write(line + '\n') 360 outfd.write(line + '\n')
355 self.blank_count = 0 361 self.blank_count = 0
356 self.Finalize() 362 self.Finalize()
357 363
358 364
359 def GetMetaDataForList(commit_range, git_dir=None, count=None, 365 def GetMetaDataForList(commit_range, git_dir=None, count=None,
360 series = Series()): 366 series = Series()):
361 """Reads out patch series metadata from the commits 367 """Reads out patch series metadata from the commits
362 368
363 This does a 'git log' on the relevant commits and pulls out the tags we 369 This does a 'git log' on the relevant commits and pulls out the tags we
364 are interested in. 370 are interested in.
365 371
366 Args: 372 Args:
367 commit_range: Range of commits to count (e.g. 'HEAD..base') 373 commit_range: Range of commits to count (e.g. 'HEAD..base')
368 git_dir: Path to git repositiory (None to use default) 374 git_dir: Path to git repositiory (None to use default)
369 count: Number of commits to list, or None for no limit 375 count: Number of commits to list, or None for no limit
370 series: Series object to add information into. By default a new series 376 series: Series object to add information into. By default a new series
371 is started. 377 is started.
372 Returns: 378 Returns:
373 A Series object containing information about the commits. 379 A Series object containing information about the commits.
374 """ 380 """
375 params = ['git', 'log', '--no-color', '--reverse', '--no-decorate', 381 params = ['git', 'log', '--no-color', '--reverse', '--no-decorate',
376 commit_range] 382 commit_range]
377 if count is not None: 383 if count is not None:
378 params[2:2] = ['-n%d' % count] 384 params[2:2] = ['-n%d' % count]
379 if git_dir: 385 if git_dir:
380 params[1:1] = ['--git-dir', git_dir] 386 params[1:1] = ['--git-dir', git_dir]
381 pipe = [params] 387 pipe = [params]
382 stdout = command.RunPipe(pipe, capture=True).stdout 388 stdout = command.RunPipe(pipe, capture=True).stdout
383 ps = PatchStream(series, is_log=True) 389 ps = PatchStream(series, is_log=True)
384 for line in stdout.splitlines(): 390 for line in stdout.splitlines():
385 ps.ProcessLine(line) 391 ps.ProcessLine(line)
386 ps.Finalize() 392 ps.Finalize()
387 return series 393 return series
388 394
389 def GetMetaData(start, count): 395 def GetMetaData(start, count):
390 """Reads out patch series metadata from the commits 396 """Reads out patch series metadata from the commits
391 397
392 This does a 'git log' on the relevant commits and pulls out the tags we 398 This does a 'git log' on the relevant commits and pulls out the tags we
393 are interested in. 399 are interested in.
394 400
395 Args: 401 Args:
396 start: Commit to start from: 0=HEAD, 1=next one, etc. 402 start: Commit to start from: 0=HEAD, 1=next one, etc.
397 count: Number of commits to list 403 count: Number of commits to list
398 """ 404 """
399 return GetMetaDataForList('HEAD~%d' % start, None, count) 405 return GetMetaDataForList('HEAD~%d' % start, None, count)
400 406
401 def FixPatch(backup_dir, fname, series, commit): 407 def FixPatch(backup_dir, fname, series, commit):
402 """Fix up a patch file, by adding/removing as required. 408 """Fix up a patch file, by adding/removing as required.
403 409
404 We remove our tags from the patch file, insert changes lists, etc. 410 We remove our tags from the patch file, insert changes lists, etc.
405 The patch file is processed in place, and overwritten. 411 The patch file is processed in place, and overwritten.
406 412
407 A backup file is put into backup_dir (if not None). 413 A backup file is put into backup_dir (if not None).
408 414
409 Args: 415 Args:
410 fname: Filename to patch file to process 416 fname: Filename to patch file to process
411 series: Series information about this patch set 417 series: Series information about this patch set
412 commit: Commit object for this patch file 418 commit: Commit object for this patch file
413 Return: 419 Return:
414 A list of errors, or [] if all ok. 420 A list of errors, or [] if all ok.
415 """ 421 """
416 handle, tmpname = tempfile.mkstemp() 422 handle, tmpname = tempfile.mkstemp()
417 outfd = os.fdopen(handle, 'w') 423 outfd = os.fdopen(handle, 'w')
418 infd = open(fname, 'r') 424 infd = open(fname, 'r')
419 ps = PatchStream(series) 425 ps = PatchStream(series)
420 ps.commit = commit 426 ps.commit = commit
421 ps.ProcessStream(infd, outfd) 427 ps.ProcessStream(infd, outfd)
422 infd.close() 428 infd.close()
423 outfd.close() 429 outfd.close()
424 430
425 # Create a backup file if required 431 # Create a backup file if required
426 if backup_dir: 432 if backup_dir:
427 shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname))) 433 shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname)))
428 shutil.move(tmpname, fname) 434 shutil.move(tmpname, fname)
429 return ps.warn 435 return ps.warn
430 436
431 def FixPatches(series, fnames): 437 def FixPatches(series, fnames):
432 """Fix up a list of patches identified by filenames 438 """Fix up a list of patches identified by filenames
433 439
434 The patch files are processed in place, and overwritten. 440 The patch files are processed in place, and overwritten.
435 441
436 Args: 442 Args:
437 series: The series object 443 series: The series object
438 fnames: List of patch files to process 444 fnames: List of patch files to process
439 """ 445 """
440 # Current workflow creates patches, so we shouldn't need a backup 446 # Current workflow creates patches, so we shouldn't need a backup
441 backup_dir = None #tempfile.mkdtemp('clean-patch') 447 backup_dir = None #tempfile.mkdtemp('clean-patch')
442 count = 0 448 count = 0
443 for fname in fnames: 449 for fname in fnames:
444 commit = series.commits[count] 450 commit = series.commits[count]
445 commit.patch = fname 451 commit.patch = fname
446 result = FixPatch(backup_dir, fname, series, commit) 452 result = FixPatch(backup_dir, fname, series, commit)
447 if result: 453 if result:
448 print '%d warnings for %s:' % (len(result), fname) 454 print '%d warnings for %s:' % (len(result), fname)
449 for warn in result: 455 for warn in result:
450 print '\t', warn 456 print '\t', warn
451 print 457 print
452 count += 1 458 count += 1
453 print 'Cleaned %d patches' % count 459 print 'Cleaned %d patches' % count
454 return series 460 return series
455 461
456 def InsertCoverLetter(fname, series, count): 462 def InsertCoverLetter(fname, series, count):
457 """Inserts a cover letter with the required info into patch 0 463 """Inserts a cover letter with the required info into patch 0
458 464
459 Args: 465 Args:
460 fname: Input / output filename of the cover letter file 466 fname: Input / output filename of the cover letter file
461 series: Series object 467 series: Series object
462 count: Number of patches in the series 468 count: Number of patches in the series
463 """ 469 """
464 fd = open(fname, 'r') 470 fd = open(fname, 'r')
465 lines = fd.readlines() 471 lines = fd.readlines()
466 fd.close() 472 fd.close()
467 473
468 fd = open(fname, 'w') 474 fd = open(fname, 'w')
469 text = series.cover 475 text = series.cover
470 prefix = series.GetPatchPrefix() 476 prefix = series.GetPatchPrefix()
471 for line in lines: 477 for line in lines:
472 if line.startswith('Subject:'): 478 if line.startswith('Subject:'):
473 # TODO: if more than 10 patches this should save 00/xx, not 0/xx 479 # TODO: if more than 10 patches this should save 00/xx, not 0/xx
474 line = 'Subject: [%s 0/%d] %s\n' % (prefix, count, text[0]) 480 line = 'Subject: [%s 0/%d] %s\n' % (prefix, count, text[0])
475 481
476 # Insert our cover letter 482 # Insert our cover letter
477 elif line.startswith('*** BLURB HERE ***'): 483 elif line.startswith('*** BLURB HERE ***'):
478 # First the blurb test 484 # First the blurb test
479 line = '\n'.join(text[1:]) + '\n' 485 line = '\n'.join(text[1:]) + '\n'
480 if series.get('notes'): 486 if series.get('notes'):
481 line += '\n'.join(series.notes) + '\n' 487 line += '\n'.join(series.notes) + '\n'
482 488
483 # Now the change list 489 # Now the change list
484 out = series.MakeChangeLog(None) 490 out = series.MakeChangeLog(None)
485 line += '\n' + '\n'.join(out) 491 line += '\n' + '\n'.join(out)
486 fd.write(line) 492 fd.write(line)
487 fd.close() 493 fd.close()
488 494