##// END OF EJS Templates
Don't let ui.username override web.contact (issue900)...
Thomas Arendsen Hein -
r5779:e9f68860 default
parent child Browse files
Show More
@@ -1,588 +1,588 b''
1 1 HGRC(5)
2 2 =======
3 3 Bryan O'Sullivan <bos@serpentine.com>
4 4
5 5 NAME
6 6 ----
7 7 hgrc - configuration files for Mercurial
8 8
9 9 SYNOPSIS
10 10 --------
11 11
12 12 The Mercurial system uses a set of configuration files to control
13 13 aspects of its behaviour.
14 14
15 15 FILES
16 16 -----
17 17
18 18 Mercurial reads configuration data from several files, if they exist.
19 19 The names of these files depend on the system on which Mercurial is
20 20 installed. Windows registry keys contain PATH-like strings, every
21 21 part must reference a Mercurial.ini file or be a directory where *.rc
22 22 files will be read.
23 23
24 24 (Unix) <install-root>/etc/mercurial/hgrc.d/*.rc::
25 25 (Unix) <install-root>/etc/mercurial/hgrc::
26 26 Per-installation configuration files, searched for in the
27 27 directory where Mercurial is installed. For example, if installed
28 28 in /shared/tools, Mercurial will look in
29 29 /shared/tools/etc/mercurial/hgrc. Options in these files apply to
30 30 all Mercurial commands executed by any user in any directory.
31 31
32 32 (Unix) /etc/mercurial/hgrc.d/*.rc::
33 33 (Unix) /etc/mercurial/hgrc::
34 34 (Windows) HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial::
35 35 or::
36 36 (Windows) C:\Mercurial\Mercurial.ini::
37 37 Per-system configuration files, for the system on which Mercurial
38 38 is running. Options in these files apply to all Mercurial
39 39 commands executed by any user in any directory. Options in these
40 40 files override per-installation options.
41 41
42 42 (Unix) $HOME/.hgrc::
43 43 (Windows) C:\Documents and Settings\USERNAME\Mercurial.ini::
44 44 (Windows) $HOME\Mercurial.ini::
45 45 Per-user configuration file, for the user running Mercurial.
46 46 Options in this file apply to all Mercurial commands executed by
47 47 any user in any directory. Options in this file override
48 48 per-installation and per-system options.
49 49 On Windows system, one of these is chosen exclusively according
50 50 to definition of HOME environment variable.
51 51
52 52 (Unix, Windows) <repo>/.hg/hgrc::
53 53 Per-repository configuration options that only apply in a
54 54 particular repository. This file is not version-controlled, and
55 55 will not get transferred during a "clone" operation. Options in
56 56 this file override options in all other configuration files.
57 57 On Unix, most of this file will be ignored if it doesn't belong
58 58 to a trusted user or to a trusted group. See the documentation
59 59 for the trusted section below for more details.
60 60
61 61 SYNTAX
62 62 ------
63 63
64 64 A configuration file consists of sections, led by a "[section]" header
65 65 and followed by "name: value" entries; "name=value" is also accepted.
66 66
67 67 [spam]
68 68 eggs=ham
69 69 green=
70 70 eggs
71 71
72 72 Each line contains one entry. If the lines that follow are indented,
73 73 they are treated as continuations of that entry.
74 74
75 75 Leading whitespace is removed from values. Empty lines are skipped.
76 76
77 77 The optional values can contain format strings which refer to other
78 78 values in the same section, or values in a special DEFAULT section.
79 79
80 80 Lines beginning with "#" or ";" are ignored and may be used to provide
81 81 comments.
82 82
83 83 SECTIONS
84 84 --------
85 85
86 86 This section describes the different sections that may appear in a
87 87 Mercurial "hgrc" file, the purpose of each section, its possible
88 88 keys, and their possible values.
89 89
90 90 decode/encode::
91 91 Filters for transforming files on checkout/checkin. This would
92 92 typically be used for newline processing or other
93 93 localization/canonicalization of files.
94 94
95 95 Filters consist of a filter pattern followed by a filter command.
96 96 Filter patterns are globs by default, rooted at the repository
97 97 root. For example, to match any file ending in ".txt" in the root
98 98 directory only, use the pattern "*.txt". To match any file ending
99 99 in ".c" anywhere in the repository, use the pattern "**.c".
100 100
101 101 The filter command can start with a specifier, either "pipe:" or
102 102 "tempfile:". If no specifier is given, "pipe:" is used by default.
103 103
104 104 A "pipe:" command must accept data on stdin and return the
105 105 transformed data on stdout.
106 106
107 107 Pipe example:
108 108
109 109 [encode]
110 110 # uncompress gzip files on checkin to improve delta compression
111 111 # note: not necessarily a good idea, just an example
112 112 *.gz = pipe: gunzip
113 113
114 114 [decode]
115 115 # recompress gzip files when writing them to the working dir (we
116 116 # can safely omit "pipe:", because it's the default)
117 117 *.gz = gzip
118 118
119 119 A "tempfile:" command is a template. The string INFILE is replaced
120 120 with the name of a temporary file that contains the data to be
121 121 filtered by the command. The string OUTFILE is replaced with the
122 122 name of an empty temporary file, where the filtered data must be
123 123 written by the command.
124 124
125 125 NOTE: the tempfile mechanism is recommended for Windows systems,
126 126 where the standard shell I/O redirection operators often have
127 127 strange effects and may corrupt the contents of your files.
128 128
129 129 The most common usage is for LF <-> CRLF translation on Windows.
130 130 For this, use the "smart" convertors which check for binary files:
131 131
132 132 [extensions]
133 133 hgext.win32text =
134 134 [encode]
135 135 ** = cleverencode:
136 136 [decode]
137 137 ** = cleverdecode:
138 138
139 139 or if you only want to translate certain files:
140 140
141 141 [extensions]
142 142 hgext.win32text =
143 143 [encode]
144 144 **.txt = dumbencode:
145 145 [decode]
146 146 **.txt = dumbdecode:
147 147
148 148 defaults::
149 149 Use the [defaults] section to define command defaults, i.e. the
150 150 default options/arguments to pass to the specified commands.
151 151
152 152 The following example makes 'hg log' run in verbose mode, and
153 153 'hg status' show only the modified files, by default.
154 154
155 155 [defaults]
156 156 log = -v
157 157 status = -m
158 158
159 159 The actual commands, instead of their aliases, must be used when
160 160 defining command defaults. The command defaults will also be
161 161 applied to the aliases of the commands defined.
162 162
163 163 diff::
164 164 Settings used when displaying diffs. They are all boolean and
165 165 defaults to False.
166 166 git;;
167 167 Use git extended diff format.
168 168 nodates;;
169 169 Don't include dates in diff headers.
170 170 showfunc;;
171 171 Show which function each change is in.
172 172 ignorews;;
173 173 Ignore white space when comparing lines.
174 174 ignorewsamount;;
175 175 Ignore changes in the amount of white space.
176 176 ignoreblanklines;;
177 177 Ignore changes whose lines are all blank.
178 178
179 179 email::
180 180 Settings for extensions that send email messages.
181 181 from;;
182 182 Optional. Email address to use in "From" header and SMTP envelope
183 183 of outgoing messages.
184 184 to;;
185 185 Optional. Comma-separated list of recipients' email addresses.
186 186 cc;;
187 187 Optional. Comma-separated list of carbon copy recipients'
188 188 email addresses.
189 189 bcc;;
190 190 Optional. Comma-separated list of blind carbon copy
191 191 recipients' email addresses. Cannot be set interactively.
192 192 method;;
193 193 Optional. Method to use to send email messages. If value is
194 194 "smtp" (default), use SMTP (see section "[smtp]" for
195 195 configuration). Otherwise, use as name of program to run that
196 196 acts like sendmail (takes "-f" option for sender, list of
197 197 recipients on command line, message on stdin). Normally, setting
198 198 this to "sendmail" or "/usr/sbin/sendmail" is enough to use
199 199 sendmail to send messages.
200 200
201 201 Email example:
202 202
203 203 [email]
204 204 from = Joseph User <joe.user@example.com>
205 205 method = /usr/sbin/sendmail
206 206
207 207 extensions::
208 208 Mercurial has an extension mechanism for adding new features. To
209 209 enable an extension, create an entry for it in this section.
210 210
211 211 If you know that the extension is already in Python's search path,
212 212 you can give the name of the module, followed by "=", with nothing
213 213 after the "=".
214 214
215 215 Otherwise, give a name that you choose, followed by "=", followed by
216 216 the path to the ".py" file (including the file name extension) that
217 217 defines the extension.
218 218
219 219 Example for ~/.hgrc:
220 220
221 221 [extensions]
222 222 # (the mq extension will get loaded from mercurial's path)
223 223 hgext.mq =
224 224 # (this extension will get loaded from the file specified)
225 225 myfeature = ~/.hgext/myfeature.py
226 226
227 227 format::
228 228
229 229 usestore;;
230 230 Enable or disable the "store" repository format which improves
231 231 compatibility with systems that fold case or otherwise mangle
232 232 filenames. Enabled by default. Disabling this option will allow
233 233 you to store longer filenames in some situations at the expense of
234 234 compatibility.
235 235
236 236 hooks::
237 237 Commands or Python functions that get automatically executed by
238 238 various actions such as starting or finishing a commit. Multiple
239 239 hooks can be run for the same action by appending a suffix to the
240 240 action. Overriding a site-wide hook can be done by changing its
241 241 value or setting it to an empty string.
242 242
243 243 Example .hg/hgrc:
244 244
245 245 [hooks]
246 246 # do not use the site-wide hook
247 247 incoming =
248 248 incoming.email = /my/email/hook
249 249 incoming.autobuild = /my/build/hook
250 250
251 251 Most hooks are run with environment variables set that give added
252 252 useful information. For each hook below, the environment variables
253 253 it is passed are listed with names of the form "$HG_foo".
254 254
255 255 changegroup;;
256 256 Run after a changegroup has been added via push, pull or
257 257 unbundle. ID of the first new changeset is in $HG_NODE. URL from
258 258 which changes came is in $HG_URL.
259 259 commit;;
260 260 Run after a changeset has been created in the local repository.
261 261 ID of the newly created changeset is in $HG_NODE. Parent
262 262 changeset IDs are in $HG_PARENT1 and $HG_PARENT2.
263 263 incoming;;
264 264 Run after a changeset has been pulled, pushed, or unbundled into
265 265 the local repository. The ID of the newly arrived changeset is in
266 266 $HG_NODE. URL that was source of changes came is in $HG_URL.
267 267 outgoing;;
268 268 Run after sending changes from local repository to another. ID of
269 269 first changeset sent is in $HG_NODE. Source of operation is in
270 270 $HG_SOURCE; see "preoutgoing" hook for description.
271 271 post-<command>;;
272 272 Run after successful invocations of the associated command. The
273 273 contents of the command line are passed as $HG_ARGS and the result
274 274 code in $HG_RESULT. Hook failure is ignored.
275 275 pre-<command>;;
276 276 Run before executing the associated command. The contents of the
277 277 command line are passed as $HG_ARGS. If the hook returns failure,
278 278 the command doesn't execute and Mercurial returns the failure code.
279 279 prechangegroup;;
280 280 Run before a changegroup is added via push, pull or unbundle.
281 281 Exit status 0 allows the changegroup to proceed. Non-zero status
282 282 will cause the push, pull or unbundle to fail. URL from which
283 283 changes will come is in $HG_URL.
284 284 precommit;;
285 285 Run before starting a local commit. Exit status 0 allows the
286 286 commit to proceed. Non-zero status will cause the commit to fail.
287 287 Parent changeset IDs are in $HG_PARENT1 and $HG_PARENT2.
288 288 preoutgoing;;
289 289 Run before collecting changes to send from the local repository to
290 290 another. Non-zero status will cause failure. This lets you
291 291 prevent pull over http or ssh. Also prevents against local pull,
292 292 push (outbound) or bundle commands, but not effective, since you
293 293 can just copy files instead then. Source of operation is in
294 294 $HG_SOURCE. If "serve", operation is happening on behalf of
295 295 remote ssh or http repository. If "push", "pull" or "bundle",
296 296 operation is happening on behalf of repository on same system.
297 297 pretag;;
298 298 Run before creating a tag. Exit status 0 allows the tag to be
299 299 created. Non-zero status will cause the tag to fail. ID of
300 300 changeset to tag is in $HG_NODE. Name of tag is in $HG_TAG. Tag
301 301 is local if $HG_LOCAL=1, in repo if $HG_LOCAL=0.
302 302 pretxnchangegroup;;
303 303 Run after a changegroup has been added via push, pull or unbundle,
304 304 but before the transaction has been committed. Changegroup is
305 305 visible to hook program. This lets you validate incoming changes
306 306 before accepting them. Passed the ID of the first new changeset
307 307 in $HG_NODE. Exit status 0 allows the transaction to commit.
308 308 Non-zero status will cause the transaction to be rolled back and
309 309 the push, pull or unbundle will fail. URL that was source of
310 310 changes is in $HG_URL.
311 311 pretxncommit;;
312 312 Run after a changeset has been created but the transaction not yet
313 313 committed. Changeset is visible to hook program. This lets you
314 314 validate commit message and changes. Exit status 0 allows the
315 315 commit to proceed. Non-zero status will cause the transaction to
316 316 be rolled back. ID of changeset is in $HG_NODE. Parent changeset
317 317 IDs are in $HG_PARENT1 and $HG_PARENT2.
318 318 preupdate;;
319 319 Run before updating the working directory. Exit status 0 allows
320 320 the update to proceed. Non-zero status will prevent the update.
321 321 Changeset ID of first new parent is in $HG_PARENT1. If merge, ID
322 322 of second new parent is in $HG_PARENT2.
323 323 tag;;
324 324 Run after a tag is created. ID of tagged changeset is in
325 325 $HG_NODE. Name of tag is in $HG_TAG. Tag is local if
326 326 $HG_LOCAL=1, in repo if $HG_LOCAL=0.
327 327 update;;
328 328 Run after updating the working directory. Changeset ID of first
329 329 new parent is in $HG_PARENT1. If merge, ID of second new parent
330 330 is in $HG_PARENT2. If update succeeded, $HG_ERROR=0. If update
331 331 failed (e.g. because conflicts not resolved), $HG_ERROR=1.
332 332
333 333 Note: it is generally better to use standard hooks rather than the
334 334 generic pre- and post- command hooks as they are guaranteed to be
335 335 called in the appropriate contexts for influencing transactions.
336 336 Also, hooks like "commit" will be called in all contexts that
337 337 generate a commit (eg. tag) and not just the commit command.
338 338
339 339 Note2: Environment variables with empty values may not be passed to
340 340 hooks on platforms like Windows. For instance, $HG_PARENT2 will
341 341 not be available under Windows for non-merge changesets while being
342 342 set to an empty value under Unix-like systems.
343 343
344 344 The syntax for Python hooks is as follows:
345 345
346 346 hookname = python:modulename.submodule.callable
347 347
348 348 Python hooks are run within the Mercurial process. Each hook is
349 349 called with at least three keyword arguments: a ui object (keyword
350 350 "ui"), a repository object (keyword "repo"), and a "hooktype"
351 351 keyword that tells what kind of hook is used. Arguments listed as
352 352 environment variables above are passed as keyword arguments, with no
353 353 "HG_" prefix, and names in lower case.
354 354
355 355 If a Python hook returns a "true" value or raises an exception, this
356 356 is treated as failure of the hook.
357 357
358 358 http_proxy::
359 359 Used to access web-based Mercurial repositories through a HTTP
360 360 proxy.
361 361 host;;
362 362 Host name and (optional) port of the proxy server, for example
363 363 "myproxy:8000".
364 364 no;;
365 365 Optional. Comma-separated list of host names that should bypass
366 366 the proxy.
367 367 passwd;;
368 368 Optional. Password to authenticate with at the proxy server.
369 369 user;;
370 370 Optional. User name to authenticate with at the proxy server.
371 371
372 372 smtp::
373 373 Configuration for extensions that need to send email messages.
374 374 host;;
375 375 Host name of mail server, e.g. "mail.example.com".
376 376 port;;
377 377 Optional. Port to connect to on mail server. Default: 25.
378 378 tls;;
379 379 Optional. Whether to connect to mail server using TLS. True or
380 380 False. Default: False.
381 381 username;;
382 382 Optional. User name to authenticate to SMTP server with.
383 383 If username is specified, password must also be specified.
384 384 Default: none.
385 385 password;;
386 386 Optional. Password to authenticate to SMTP server with.
387 387 If username is specified, password must also be specified.
388 388 Default: none.
389 389 local_hostname;;
390 390 Optional. It's the hostname that the sender can use to identify itself
391 391 to the MTA.
392 392
393 393 paths::
394 394 Assigns symbolic names to repositories. The left side is the
395 395 symbolic name, and the right gives the directory or URL that is the
396 396 location of the repository. Default paths can be declared by
397 397 setting the following entries.
398 398 default;;
399 399 Directory or URL to use when pulling if no source is specified.
400 400 Default is set to repository from which the current repository
401 401 was cloned.
402 402 default-push;;
403 403 Optional. Directory or URL to use when pushing if no destination
404 404 is specified.
405 405
406 406 server::
407 407 Controls generic server settings.
408 408 uncompressed;;
409 409 Whether to allow clients to clone a repo using the uncompressed
410 410 streaming protocol. This transfers about 40% more data than a
411 411 regular clone, but uses less memory and CPU on both server and
412 412 client. Over a LAN (100Mbps or better) or a very fast WAN, an
413 413 uncompressed streaming clone is a lot faster (~10x) than a regular
414 414 clone. Over most WAN connections (anything slower than about
415 415 6Mbps), uncompressed streaming is slower, because of the extra
416 416 data transfer overhead. Default is False.
417 417
418 418 trusted::
419 419 For security reasons, Mercurial will not use the settings in
420 420 the .hg/hgrc file from a repository if it doesn't belong to a
421 421 trusted user or to a trusted group. The main exception is the
422 422 web interface, which automatically uses some safe settings, since
423 423 it's common to serve repositories from different users.
424 424
425 425 This section specifies what users and groups are trusted. The
426 426 current user is always trusted. To trust everybody, list a user
427 427 or a group with name "*".
428 428
429 429 users;;
430 430 Comma-separated list of trusted users.
431 431 groups;;
432 432 Comma-separated list of trusted groups.
433 433
434 434 ui::
435 435 User interface controls.
436 436 debug;;
437 437 Print debugging information. True or False. Default is False.
438 438 editor;;
439 439 The editor to use during a commit. Default is $EDITOR or "vi".
440 440 fallbackencoding;;
441 441 Encoding to try if it's not possible to decode the changelog using
442 442 UTF-8. Default is ISO-8859-1.
443 443 ignore;;
444 444 A file to read per-user ignore patterns from. This file should be in
445 445 the same format as a repository-wide .hgignore file. This option
446 446 supports hook syntax, so if you want to specify multiple ignore
447 447 files, you can do so by setting something like
448 448 "ignore.other = ~/.hgignore2". For details of the ignore file
449 449 format, see the hgignore(5) man page.
450 450 interactive;;
451 451 Allow to prompt the user. True or False. Default is True.
452 452 logtemplate;;
453 453 Template string for commands that print changesets.
454 454 merge;;
455 455 The conflict resolution program to use during a manual merge.
456 456 Default is "hgmerge".
457 457 patch;;
458 458 command to use to apply patches. Look for 'gpatch' or 'patch' in PATH if
459 459 unset.
460 460 quiet;;
461 461 Reduce the amount of output printed. True or False. Default is False.
462 462 remotecmd;;
463 463 remote command to use for clone/push/pull operations. Default is 'hg'.
464 464 report_untrusted;;
465 465 Warn if a .hg/hgrc file is ignored due to not being owned by a
466 466 trusted user or group. True or False. Default is True.
467 467 slash;;
468 468 Display paths using a slash ("/") as the path separator. This only
469 469 makes a difference on systems where the default path separator is not
470 470 the slash character (e.g. Windows uses the backslash character ("\")).
471 471 Default is False.
472 472 ssh;;
473 473 command to use for SSH connections. Default is 'ssh'.
474 474 strict;;
475 475 Require exact command names, instead of allowing unambiguous
476 476 abbreviations. True or False. Default is False.
477 477 style;;
478 478 Name of style to use for command output.
479 479 timeout;;
480 480 The timeout used when a lock is held (in seconds), a negative value
481 481 means no timeout. Default is 600.
482 482 username;;
483 483 The committer of a changeset created when running "commit".
484 484 Typically a person's name and email address, e.g. "Fred Widget
485 485 <fred@example.com>". Default is $EMAIL or username@hostname.
486 486 If the username in hgrc is empty, it has to be specified manually or
487 487 in a different hgrc file (e.g. $HOME/.hgrc, if the admin set "username ="
488 488 in the system hgrc).
489 489 verbose;;
490 490 Increase the amount of output printed. True or False. Default is False.
491 491
492 492
493 493 web::
494 494 Web interface configuration.
495 495 accesslog;;
496 496 Where to output the access log. Default is stdout.
497 497 address;;
498 498 Interface address to bind to. Default is all.
499 499 allow_archive;;
500 500 List of archive format (bz2, gz, zip) allowed for downloading.
501 501 Default is empty.
502 502 allowbz2;;
503 503 (DEPRECATED) Whether to allow .tar.bz2 downloading of repo revisions.
504 504 Default is false.
505 505 allowgz;;
506 506 (DEPRECATED) Whether to allow .tar.gz downloading of repo revisions.
507 507 Default is false.
508 508 allowpull;;
509 509 Whether to allow pulling from the repository. Default is true.
510 510 allow_push;;
511 511 Whether to allow pushing to the repository. If empty or not set,
512 512 push is not allowed. If the special value "*", any remote user
513 513 can push, including unauthenticated users. Otherwise, the remote
514 514 user must have been authenticated, and the authenticated user name
515 515 must be present in this list (separated by whitespace or ",").
516 516 The contents of the allow_push list are examined after the
517 517 deny_push list.
518 518 allowzip;;
519 519 (DEPRECATED) Whether to allow .zip downloading of repo revisions.
520 520 Default is false. This feature creates temporary files.
521 521 baseurl;;
522 522 Base URL to use when publishing URLs in other locations, so
523 523 third-party tools like email notification hooks can construct URLs.
524 524 Example: "http://hgserver/repos/"
525 525 contact;;
526 526 Name or email address of the person in charge of the repository.
527 Default is "unknown".
527 Defaults to ui.username or $EMAIL or "unknown" if unset or empty.
528 528 deny_push;;
529 529 Whether to deny pushing to the repository. If empty or not set,
530 530 push is not denied. If the special value "*", all remote users
531 531 are denied push. Otherwise, unauthenticated users are all denied,
532 532 and any authenticated user name present in this list (separated by
533 533 whitespace or ",") is also denied. The contents of the deny_push
534 534 list are examined before the allow_push list.
535 535 description;;
536 536 Textual description of the repository's purpose or contents.
537 537 Default is "unknown".
538 538 encoding;;
539 539 Character encoding name.
540 540 Example: "UTF-8"
541 541 errorlog;;
542 542 Where to output the error log. Default is stderr.
543 543 hidden;;
544 544 Whether to hide the repository in the hgwebdir index. Default is false.
545 545 ipv6;;
546 546 Whether to use IPv6. Default is false.
547 547 name;;
548 548 Repository name to use in the web interface. Default is current
549 549 working directory.
550 550 maxchanges;;
551 551 Maximum number of changes to list on the changelog. Default is 10.
552 552 maxfiles;;
553 553 Maximum number of files to list per changeset. Default is 10.
554 554 port;;
555 555 Port to listen on. Default is 8000.
556 556 push_ssl;;
557 557 Whether to require that inbound pushes be transported over SSL to
558 558 prevent password sniffing. Default is true.
559 559 staticurl;;
560 560 Base URL to use for static files. If unset, static files (e.g.
561 561 the hgicon.png favicon) will be served by the CGI script itself.
562 562 Use this setting to serve them directly with the HTTP server.
563 563 Example: "http://hgserver/static/"
564 564 stripes;;
565 565 How many lines a "zebra stripe" should span in multiline output.
566 566 Default is 1; set to 0 to disable.
567 567 style;;
568 568 Which template map style to use.
569 569 templates;;
570 570 Where to find the HTML templates. Default is install path.
571 571
572 572
573 573 AUTHOR
574 574 ------
575 575 Bryan O'Sullivan <bos@serpentine.com>.
576 576
577 577 Mercurial was written by Matt Mackall <mpm@selenic.com>.
578 578
579 579 SEE ALSO
580 580 --------
581 581 hg(1), hgignore(5)
582 582
583 583 COPYING
584 584 -------
585 585 This manual page is copyright 2005 Bryan O'Sullivan.
586 586 Mercurial is copyright 2005-2007 Matt Mackall.
587 587 Free use of this software is granted under the terms of the GNU General
588 588 Public License (GPL).
@@ -1,99 +1,108 b''
1 1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import errno, mimetypes, os
10 10
11 11 class ErrorResponse(Exception):
12 12 def __init__(self, code, message=None):
13 13 Exception.__init__(self)
14 14 self.code = code
15 15 if message:
16 16 self.message = message
17 17 else:
18 18 self.message = _statusmessage(code)
19 19
20 20 def _statusmessage(code):
21 21 from BaseHTTPServer import BaseHTTPRequestHandler
22 22 responses = BaseHTTPRequestHandler.responses
23 23 return responses.get(code, ('Error', 'Unknown error'))[0]
24 24
25 25 def statusmessage(code):
26 26 return '%d %s' % (code, _statusmessage(code))
27 27
28 28 def get_mtime(repo_path):
29 29 store_path = os.path.join(repo_path, ".hg")
30 30 if not os.path.isdir(os.path.join(store_path, "data")):
31 31 store_path = os.path.join(store_path, "store")
32 32 cl_path = os.path.join(store_path, "00changelog.i")
33 33 if os.path.exists(cl_path):
34 34 return os.stat(cl_path).st_mtime
35 35 else:
36 36 return os.stat(store_path).st_mtime
37 37
38 38 def staticfile(directory, fname, req):
39 39 """return a file inside directory with guessed content-type header
40 40
41 41 fname always uses '/' as directory separator and isn't allowed to
42 42 contain unusual path components.
43 43 Content-type is guessed using the mimetypes module.
44 44 Return an empty string if fname is illegal or file not found.
45 45
46 46 """
47 47 parts = fname.split('/')
48 48 path = directory
49 49 for part in parts:
50 50 if (part in ('', os.curdir, os.pardir) or
51 51 os.sep in part or os.altsep is not None and os.altsep in part):
52 52 return ""
53 53 path = os.path.join(path, part)
54 54 try:
55 55 os.stat(path)
56 56 ct = mimetypes.guess_type(path)[0] or "text/plain"
57 57 req.header([('Content-type', ct),
58 58 ('Content-length', str(os.path.getsize(path)))])
59 59 return file(path, 'rb').read()
60 60 except TypeError:
61 61 raise ErrorResponse(500, 'illegal file name')
62 62 except OSError, err:
63 63 if err.errno == errno.ENOENT:
64 64 raise ErrorResponse(404)
65 65 else:
66 66 raise ErrorResponse(500, err.strerror)
67 67
68 68 def style_map(templatepath, style):
69 69 """Return path to mapfile for a given style.
70 70
71 71 Searches mapfile in the following locations:
72 72 1. templatepath/style/map
73 73 2. templatepath/map-style
74 74 3. templatepath/map
75 75 """
76 76 locations = style and [os.path.join(style, "map"), "map-"+style] or []
77 77 locations.append("map")
78 78 for location in locations:
79 79 mapfile = os.path.join(templatepath, location)
80 80 if os.path.isfile(mapfile):
81 81 return mapfile
82 82 raise RuntimeError("No hgweb templates found in %r" % templatepath)
83 83
84 84 def paritygen(stripecount, offset=0):
85 85 """count parity of horizontal stripes for easier reading"""
86 86 if stripecount and offset:
87 87 # account for offset, e.g. due to building the list in reverse
88 88 count = (stripecount + offset) % stripecount
89 89 parity = (stripecount + offset) / stripecount & 1
90 90 else:
91 91 count = 0
92 92 parity = 0
93 93 while True:
94 94 yield parity
95 95 count += 1
96 96 if stripecount and count >= stripecount:
97 97 parity = 1 - parity
98 98 count = 0
99 99
100 def get_contact(config):
101 """Return repo contact information or empty string.
102
103 web.contact is the primary source, but if that is not set, try
104 ui.username or $EMAIL as a fallback to display something useful.
105 """
106 return (config("web", "contact") or
107 config("ui", "username") or
108 os.environ.get("EMAIL") or "")
@@ -1,911 +1,909 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes, re, mimetools, cStringIO
10 10 from mercurial.node import *
11 11 from mercurial import mdiff, ui, hg, util, archival, patch
12 12 from mercurial import revlog, templater
13 from common import ErrorResponse, get_mtime, style_map, paritygen
13 from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact
14 14 from request import wsgirequest
15 15 import webcommands, protocol
16 16
17 17 shortcuts = {
18 18 'cl': [('cmd', ['changelog']), ('rev', None)],
19 19 'sl': [('cmd', ['shortlog']), ('rev', None)],
20 20 'cs': [('cmd', ['changeset']), ('node', None)],
21 21 'f': [('cmd', ['file']), ('filenode', None)],
22 22 'fl': [('cmd', ['filelog']), ('filenode', None)],
23 23 'fd': [('cmd', ['filediff']), ('node', None)],
24 24 'fa': [('cmd', ['annotate']), ('filenode', None)],
25 25 'mf': [('cmd', ['manifest']), ('manifest', None)],
26 26 'ca': [('cmd', ['archive']), ('node', None)],
27 27 'tags': [('cmd', ['tags'])],
28 28 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
29 29 'static': [('cmd', ['static']), ('file', None)]
30 30 }
31 31
32 32 def _up(p):
33 33 if p[0] != "/":
34 34 p = "/" + p
35 35 if p[-1] == "/":
36 36 p = p[:-1]
37 37 up = os.path.dirname(p)
38 38 if up == "/":
39 39 return "/"
40 40 return up + "/"
41 41
42 42 def revnavgen(pos, pagelen, limit, nodefunc):
43 43 def seq(factor, limit=None):
44 44 if limit:
45 45 yield limit
46 46 if limit >= 20 and limit <= 40:
47 47 yield 50
48 48 else:
49 49 yield 1 * factor
50 50 yield 3 * factor
51 51 for f in seq(factor * 10):
52 52 yield f
53 53
54 54 def nav(**map):
55 55 l = []
56 56 last = 0
57 57 for f in seq(1, pagelen):
58 58 if f < pagelen or f <= last:
59 59 continue
60 60 if f > limit:
61 61 break
62 62 last = f
63 63 if pos + f < limit:
64 64 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
65 65 if pos - f >= 0:
66 66 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
67 67
68 68 try:
69 69 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
70 70
71 71 for label, node in l:
72 72 yield {"label": label, "node": node}
73 73
74 74 yield {"label": "tip", "node": "tip"}
75 75 except hg.RepoError:
76 76 pass
77 77
78 78 return nav
79 79
80 80 class hgweb(object):
81 81 def __init__(self, repo, name=None):
82 82 if isinstance(repo, str):
83 83 parentui = ui.ui(report_untrusted=False, interactive=False)
84 84 self.repo = hg.repository(parentui, repo)
85 85 else:
86 86 self.repo = repo
87 87
88 88 self.mtime = -1
89 89 self.reponame = name
90 90 self.archives = 'zip', 'gz', 'bz2'
91 91 self.stripecount = 1
92 92 # a repo owner may set web.templates in .hg/hgrc to get any file
93 93 # readable by the user running the CGI script
94 94 self.templatepath = self.config("web", "templates",
95 95 templater.templatepath(),
96 96 untrusted=False)
97 97
98 98 # The CGI scripts are often run by a user different from the repo owner.
99 99 # Trust the settings from the .hg/hgrc files by default.
100 100 def config(self, section, name, default=None, untrusted=True):
101 101 return self.repo.ui.config(section, name, default,
102 102 untrusted=untrusted)
103 103
104 104 def configbool(self, section, name, default=False, untrusted=True):
105 105 return self.repo.ui.configbool(section, name, default,
106 106 untrusted=untrusted)
107 107
108 108 def configlist(self, section, name, default=None, untrusted=True):
109 109 return self.repo.ui.configlist(section, name, default,
110 110 untrusted=untrusted)
111 111
112 112 def refresh(self):
113 113 mtime = get_mtime(self.repo.root)
114 114 if mtime != self.mtime:
115 115 self.mtime = mtime
116 116 self.repo = hg.repository(self.repo.ui, self.repo.root)
117 117 self.maxchanges = int(self.config("web", "maxchanges", 10))
118 118 self.stripecount = int(self.config("web", "stripes", 1))
119 119 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
120 120 self.maxfiles = int(self.config("web", "maxfiles", 10))
121 121 self.allowpull = self.configbool("web", "allowpull", True)
122 122 self.encoding = self.config("web", "encoding", util._encoding)
123 123
124 124 def run(self):
125 125 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
126 126 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
127 127 import mercurial.hgweb.wsgicgi as wsgicgi
128 128 wsgicgi.launch(self)
129 129
130 130 def __call__(self, env, respond):
131 131 req = wsgirequest(env, respond)
132 132 self.run_wsgi(req)
133 133 return req
134 134
135 135 def run_wsgi(self, req):
136 136
137 137 self.refresh()
138 138
139 139 # expand form shortcuts
140 140
141 141 for k in shortcuts.iterkeys():
142 142 if k in req.form:
143 143 for name, value in shortcuts[k]:
144 144 if value is None:
145 145 value = req.form[k]
146 146 req.form[name] = value
147 147 del req.form[k]
148 148
149 149 # work with CGI variables to create coherent structure
150 150 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
151 151
152 152 req.url = req.env['SCRIPT_NAME']
153 153 if not req.url.endswith('/'):
154 154 req.url += '/'
155 155 if req.env.has_key('REPO_NAME'):
156 156 req.url += req.env['REPO_NAME'] + '/'
157 157
158 158 if req.env.get('PATH_INFO'):
159 159 parts = req.env.get('PATH_INFO').strip('/').split('/')
160 160 repo_parts = req.env.get('REPO_NAME', '').split('/')
161 161 if parts[:len(repo_parts)] == repo_parts:
162 162 parts = parts[len(repo_parts):]
163 163 query = '/'.join(parts)
164 164 else:
165 165 query = req.env['QUERY_STRING'].split('&', 1)[0]
166 166 query = query.split(';', 1)[0]
167 167
168 168 # translate user-visible url structure to internal structure
169 169
170 170 args = query.split('/', 2)
171 171 if 'cmd' not in req.form and args and args[0]:
172 172
173 173 cmd = args.pop(0)
174 174 style = cmd.rfind('-')
175 175 if style != -1:
176 176 req.form['style'] = [cmd[:style]]
177 177 cmd = cmd[style+1:]
178 178
179 179 # avoid accepting e.g. style parameter as command
180 180 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
181 181 req.form['cmd'] = [cmd]
182 182
183 183 if args and args[0]:
184 184 node = args.pop(0)
185 185 req.form['node'] = [node]
186 186 if args:
187 187 req.form['file'] = args
188 188
189 189 if cmd == 'static':
190 190 req.form['file'] = req.form['node']
191 191 elif cmd == 'archive':
192 192 fn = req.form['node'][0]
193 193 for type_, spec in self.archive_specs.iteritems():
194 194 ext = spec[2]
195 195 if fn.endswith(ext):
196 196 req.form['node'] = [fn[:-len(ext)]]
197 197 req.form['type'] = [type_]
198 198
199 199 # actually process the request
200 200
201 201 try:
202 202
203 203 cmd = req.form.get('cmd', [''])[0]
204 204 if hasattr(protocol, cmd):
205 205 method = getattr(protocol, cmd)
206 206 method(self, req)
207 207 else:
208 208 tmpl = self.templater(req)
209 209 if cmd == '':
210 210 req.form['cmd'] = [tmpl.cache['default']]
211 211 cmd = req.form['cmd'][0]
212 212 method = getattr(webcommands, cmd)
213 213 method(self, req, tmpl)
214 214 del tmpl
215 215
216 216 except revlog.LookupError, err:
217 217 req.respond(404, tmpl(
218 218 'error', error='revision not found: %s' % err.name))
219 219 except (hg.RepoError, revlog.RevlogError), inst:
220 220 req.respond('500 Internal Server Error',
221 221 tmpl('error', error=str(inst)))
222 222 except ErrorResponse, inst:
223 223 req.respond(inst.code, tmpl('error', error=inst.message))
224 224 except AttributeError:
225 225 req.respond(400, tmpl('error', error='No such method: ' + cmd))
226 226
227 227 def templater(self, req):
228 228
229 229 # determine scheme, port and server name
230 230 # this is needed to create absolute urls
231 231
232 232 proto = req.env.get('wsgi.url_scheme')
233 233 if proto == 'https':
234 234 proto = 'https'
235 235 default_port = "443"
236 236 else:
237 237 proto = 'http'
238 238 default_port = "80"
239 239
240 240 port = req.env["SERVER_PORT"]
241 241 port = port != default_port and (":" + port) or ""
242 242 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
243 243 staticurl = self.config("web", "staticurl") or req.url + 'static/'
244 244 if not staticurl.endswith('/'):
245 245 staticurl += '/'
246 246
247 247 # some functions for the templater
248 248
249 249 def header(**map):
250 250 header_file = cStringIO.StringIO(
251 251 ''.join(tmpl("header", encoding=self.encoding, **map)))
252 252 msg = mimetools.Message(header_file, 0)
253 253 req.header(msg.items())
254 254 yield header_file.read()
255 255
256 256 def rawfileheader(**map):
257 257 req.header([('Content-type', map['mimetype']),
258 258 ('Content-disposition', 'filename=%s' % map['file']),
259 259 ('Content-length', str(len(map['raw'])))])
260 260 yield ''
261 261
262 262 def footer(**map):
263 263 yield tmpl("footer", **map)
264 264
265 265 def motd(**map):
266 266 yield self.config("web", "motd", "")
267 267
268 268 def sessionvars(**map):
269 269 fields = []
270 270 if req.form.has_key('style'):
271 271 style = req.form['style'][0]
272 272 if style != self.config('web', 'style', ''):
273 273 fields.append(('style', style))
274 274
275 275 separator = req.url[-1] == '?' and ';' or '?'
276 276 for name, value in fields:
277 277 yield dict(name=name, value=value, separator=separator)
278 278 separator = ';'
279 279
280 280 # figure out which style to use
281 281
282 282 style = self.config("web", "style", "")
283 283 if req.form.has_key('style'):
284 284 style = req.form['style'][0]
285 285 mapfile = style_map(self.templatepath, style)
286 286
287 287 if not self.reponame:
288 288 self.reponame = (self.config("web", "name")
289 289 or req.env.get('REPO_NAME')
290 290 or req.url.strip('/') or self.repo.root)
291 291
292 292 # create the templater
293 293
294 294 tmpl = templater.templater(mapfile, templater.common_filters,
295 295 defaults={"url": req.url,
296 296 "staticurl": staticurl,
297 297 "urlbase": urlbase,
298 298 "repo": self.reponame,
299 299 "header": header,
300 300 "footer": footer,
301 301 "motd": motd,
302 302 "rawfileheader": rawfileheader,
303 303 "sessionvars": sessionvars
304 304 })
305 305 return tmpl
306 306
307 307 def archivelist(self, nodeid):
308 308 allowed = self.configlist("web", "allow_archive")
309 309 for i, spec in self.archive_specs.iteritems():
310 310 if i in allowed or self.configbool("web", "allow" + i):
311 311 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
312 312
313 313 def listfilediffs(self, tmpl, files, changeset):
314 314 for f in files[:self.maxfiles]:
315 315 yield tmpl("filedifflink", node=hex(changeset), file=f)
316 316 if len(files) > self.maxfiles:
317 317 yield tmpl("fileellipses")
318 318
319 319 def siblings(self, siblings=[], hiderev=None, **args):
320 320 siblings = [s for s in siblings if s.node() != nullid]
321 321 if len(siblings) == 1 and siblings[0].rev() == hiderev:
322 322 return
323 323 for s in siblings:
324 324 d = {'node': hex(s.node()), 'rev': s.rev()}
325 325 if hasattr(s, 'path'):
326 326 d['file'] = s.path()
327 327 d.update(args)
328 328 yield d
329 329
330 330 def renamelink(self, fl, node):
331 331 r = fl.renamed(node)
332 332 if r:
333 333 return [dict(file=r[0], node=hex(r[1]))]
334 334 return []
335 335
336 336 def nodetagsdict(self, node):
337 337 return [{"name": i} for i in self.repo.nodetags(node)]
338 338
339 339 def nodebranchdict(self, ctx):
340 340 branches = []
341 341 branch = ctx.branch()
342 342 # If this is an empty repo, ctx.node() == nullid,
343 343 # ctx.branch() == 'default', but branchtags() is
344 344 # an empty dict. Using dict.get avoids a traceback.
345 345 if self.repo.branchtags().get(branch) == ctx.node():
346 346 branches.append({"name": branch})
347 347 return branches
348 348
349 349 def showtag(self, tmpl, t1, node=nullid, **args):
350 350 for t in self.repo.nodetags(node):
351 351 yield tmpl(t1, tag=t, **args)
352 352
353 353 def diff(self, tmpl, node1, node2, files):
354 354 def filterfiles(filters, files):
355 355 l = [x for x in files if x in filters]
356 356
357 357 for t in filters:
358 358 if t and t[-1] != os.sep:
359 359 t += os.sep
360 360 l += [x for x in files if x.startswith(t)]
361 361 return l
362 362
363 363 parity = paritygen(self.stripecount)
364 364 def diffblock(diff, f, fn):
365 365 yield tmpl("diffblock",
366 366 lines=prettyprintlines(diff),
367 367 parity=parity.next(),
368 368 file=f,
369 369 filenode=hex(fn or nullid))
370 370
371 371 def prettyprintlines(diff):
372 372 for l in diff.splitlines(1):
373 373 if l.startswith('+'):
374 374 yield tmpl("difflineplus", line=l)
375 375 elif l.startswith('-'):
376 376 yield tmpl("difflineminus", line=l)
377 377 elif l.startswith('@'):
378 378 yield tmpl("difflineat", line=l)
379 379 else:
380 380 yield tmpl("diffline", line=l)
381 381
382 382 r = self.repo
383 383 c1 = r.changectx(node1)
384 384 c2 = r.changectx(node2)
385 385 date1 = util.datestr(c1.date())
386 386 date2 = util.datestr(c2.date())
387 387
388 388 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
389 389 if files:
390 390 modified, added, removed = map(lambda x: filterfiles(files, x),
391 391 (modified, added, removed))
392 392
393 393 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
394 394 for f in modified:
395 395 to = c1.filectx(f).data()
396 396 tn = c2.filectx(f).data()
397 397 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
398 398 opts=diffopts), f, tn)
399 399 for f in added:
400 400 to = None
401 401 tn = c2.filectx(f).data()
402 402 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
403 403 opts=diffopts), f, tn)
404 404 for f in removed:
405 405 to = c1.filectx(f).data()
406 406 tn = None
407 407 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
408 408 opts=diffopts), f, tn)
409 409
410 410 def changelog(self, tmpl, ctx, shortlog=False):
411 411 def changelist(limit=0,**map):
412 412 cl = self.repo.changelog
413 413 l = [] # build a list in forward order for efficiency
414 414 for i in xrange(start, end):
415 415 ctx = self.repo.changectx(i)
416 416 n = ctx.node()
417 417
418 418 l.insert(0, {"parity": parity.next(),
419 419 "author": ctx.user(),
420 420 "parent": self.siblings(ctx.parents(), i - 1),
421 421 "child": self.siblings(ctx.children(), i + 1),
422 422 "changelogtag": self.showtag("changelogtag",n),
423 423 "desc": ctx.description(),
424 424 "date": ctx.date(),
425 425 "files": self.listfilediffs(tmpl, ctx.files(), n),
426 426 "rev": i,
427 427 "node": hex(n),
428 428 "tags": self.nodetagsdict(n),
429 429 "branches": self.nodebranchdict(ctx)})
430 430
431 431 if limit > 0:
432 432 l = l[:limit]
433 433
434 434 for e in l:
435 435 yield e
436 436
437 437 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
438 438 cl = self.repo.changelog
439 439 count = cl.count()
440 440 pos = ctx.rev()
441 441 start = max(0, pos - maxchanges + 1)
442 442 end = min(count, start + maxchanges)
443 443 pos = end - 1
444 444 parity = paritygen(self.stripecount, offset=start-end)
445 445
446 446 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
447 447
448 448 yield tmpl(shortlog and 'shortlog' or 'changelog',
449 449 changenav=changenav,
450 450 node=hex(cl.tip()),
451 451 rev=pos, changesets=count,
452 452 entries=lambda **x: changelist(limit=0,**x),
453 453 latestentry=lambda **x: changelist(limit=1,**x),
454 454 archives=self.archivelist("tip"))
455 455
456 456 def search(self, tmpl, query):
457 457
458 458 def changelist(**map):
459 459 cl = self.repo.changelog
460 460 count = 0
461 461 qw = query.lower().split()
462 462
463 463 def revgen():
464 464 for i in xrange(cl.count() - 1, 0, -100):
465 465 l = []
466 466 for j in xrange(max(0, i - 100), i):
467 467 ctx = self.repo.changectx(j)
468 468 l.append(ctx)
469 469 l.reverse()
470 470 for e in l:
471 471 yield e
472 472
473 473 for ctx in revgen():
474 474 miss = 0
475 475 for q in qw:
476 476 if not (q in ctx.user().lower() or
477 477 q in ctx.description().lower() or
478 478 q in " ".join(ctx.files()).lower()):
479 479 miss = 1
480 480 break
481 481 if miss:
482 482 continue
483 483
484 484 count += 1
485 485 n = ctx.node()
486 486
487 487 yield tmpl('searchentry',
488 488 parity=parity.next(),
489 489 author=ctx.user(),
490 490 parent=self.siblings(ctx.parents()),
491 491 child=self.siblings(ctx.children()),
492 492 changelogtag=self.showtag("changelogtag",n),
493 493 desc=ctx.description(),
494 494 date=ctx.date(),
495 495 files=self.listfilediffs(tmpl, ctx.files(), n),
496 496 rev=ctx.rev(),
497 497 node=hex(n),
498 498 tags=self.nodetagsdict(n),
499 499 branches=self.nodebranchdict(ctx))
500 500
501 501 if count >= self.maxchanges:
502 502 break
503 503
504 504 cl = self.repo.changelog
505 505 parity = paritygen(self.stripecount)
506 506
507 507 yield tmpl('search',
508 508 query=query,
509 509 node=hex(cl.tip()),
510 510 entries=changelist,
511 511 archives=self.archivelist("tip"))
512 512
513 513 def changeset(self, tmpl, ctx):
514 514 n = ctx.node()
515 515 parents = ctx.parents()
516 516 p1 = parents[0].node()
517 517
518 518 files = []
519 519 parity = paritygen(self.stripecount)
520 520 for f in ctx.files():
521 521 files.append(tmpl("filenodelink",
522 522 node=hex(n), file=f,
523 523 parity=parity.next()))
524 524
525 525 def diff(**map):
526 526 yield self.diff(tmpl, p1, n, None)
527 527
528 528 yield tmpl('changeset',
529 529 diff=diff,
530 530 rev=ctx.rev(),
531 531 node=hex(n),
532 532 parent=self.siblings(parents),
533 533 child=self.siblings(ctx.children()),
534 534 changesettag=self.showtag("changesettag",n),
535 535 author=ctx.user(),
536 536 desc=ctx.description(),
537 537 date=ctx.date(),
538 538 files=files,
539 539 archives=self.archivelist(hex(n)),
540 540 tags=self.nodetagsdict(n),
541 541 branches=self.nodebranchdict(ctx))
542 542
543 543 def filelog(self, tmpl, fctx):
544 544 f = fctx.path()
545 545 fl = fctx.filelog()
546 546 count = fl.count()
547 547 pagelen = self.maxshortchanges
548 548 pos = fctx.filerev()
549 549 start = max(0, pos - pagelen + 1)
550 550 end = min(count, start + pagelen)
551 551 pos = end - 1
552 552 parity = paritygen(self.stripecount, offset=start-end)
553 553
554 554 def entries(limit=0, **map):
555 555 l = []
556 556
557 557 for i in xrange(start, end):
558 558 ctx = fctx.filectx(i)
559 559 n = fl.node(i)
560 560
561 561 l.insert(0, {"parity": parity.next(),
562 562 "filerev": i,
563 563 "file": f,
564 564 "node": hex(ctx.node()),
565 565 "author": ctx.user(),
566 566 "date": ctx.date(),
567 567 "rename": self.renamelink(fl, n),
568 568 "parent": self.siblings(fctx.parents()),
569 569 "child": self.siblings(fctx.children()),
570 570 "desc": ctx.description()})
571 571
572 572 if limit > 0:
573 573 l = l[:limit]
574 574
575 575 for e in l:
576 576 yield e
577 577
578 578 nodefunc = lambda x: fctx.filectx(fileid=x)
579 579 nav = revnavgen(pos, pagelen, count, nodefunc)
580 580 yield tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
581 581 entries=lambda **x: entries(limit=0, **x),
582 582 latestentry=lambda **x: entries(limit=1, **x))
583 583
584 584 def filerevision(self, tmpl, fctx):
585 585 f = fctx.path()
586 586 text = fctx.data()
587 587 fl = fctx.filelog()
588 588 n = fctx.filenode()
589 589 parity = paritygen(self.stripecount)
590 590
591 591 mt = mimetypes.guess_type(f)[0]
592 592 rawtext = text
593 593 if util.binary(text):
594 594 mt = mt or 'application/octet-stream'
595 595 text = "(binary:%s)" % mt
596 596 mt = mt or 'text/plain'
597 597
598 598 def lines():
599 599 for l, t in enumerate(text.splitlines(1)):
600 600 yield {"line": t,
601 601 "linenumber": "% 6d" % (l + 1),
602 602 "parity": parity.next()}
603 603
604 604 yield tmpl("filerevision",
605 605 file=f,
606 606 path=_up(f),
607 607 text=lines(),
608 608 raw=rawtext,
609 609 mimetype=mt,
610 610 rev=fctx.rev(),
611 611 node=hex(fctx.node()),
612 612 author=fctx.user(),
613 613 date=fctx.date(),
614 614 desc=fctx.description(),
615 615 parent=self.siblings(fctx.parents()),
616 616 child=self.siblings(fctx.children()),
617 617 rename=self.renamelink(fl, n),
618 618 permissions=fctx.manifest().flags(f))
619 619
620 620 def fileannotate(self, tmpl, fctx):
621 621 f = fctx.path()
622 622 n = fctx.filenode()
623 623 fl = fctx.filelog()
624 624 parity = paritygen(self.stripecount)
625 625
626 626 def annotate(**map):
627 627 last = None
628 628 for f, l in fctx.annotate(follow=True):
629 629 fnode = f.filenode()
630 630 name = self.repo.ui.shortuser(f.user())
631 631
632 632 if last != fnode:
633 633 last = fnode
634 634
635 635 yield {"parity": parity.next(),
636 636 "node": hex(f.node()),
637 637 "rev": f.rev(),
638 638 "author": name,
639 639 "file": f.path(),
640 640 "line": l}
641 641
642 642 yield tmpl("fileannotate",
643 643 file=f,
644 644 annotate=annotate,
645 645 path=_up(f),
646 646 rev=fctx.rev(),
647 647 node=hex(fctx.node()),
648 648 author=fctx.user(),
649 649 date=fctx.date(),
650 650 desc=fctx.description(),
651 651 rename=self.renamelink(fl, n),
652 652 parent=self.siblings(fctx.parents()),
653 653 child=self.siblings(fctx.children()),
654 654 permissions=fctx.manifest().flags(f))
655 655
656 656 def manifest(self, tmpl, ctx, path):
657 657 mf = ctx.manifest()
658 658 node = ctx.node()
659 659
660 660 files = {}
661 661 parity = paritygen(self.stripecount)
662 662
663 663 if path and path[-1] != "/":
664 664 path += "/"
665 665 l = len(path)
666 666 abspath = "/" + path
667 667
668 668 for f, n in mf.items():
669 669 if f[:l] != path:
670 670 continue
671 671 remain = f[l:]
672 672 if "/" in remain:
673 673 short = remain[:remain.index("/") + 1] # bleah
674 674 files[short] = (f, None)
675 675 else:
676 676 short = os.path.basename(remain)
677 677 files[short] = (f, n)
678 678
679 679 if not files:
680 680 raise ErrorResponse(404, 'Path not found: ' + path)
681 681
682 682 def filelist(**map):
683 683 fl = files.keys()
684 684 fl.sort()
685 685 for f in fl:
686 686 full, fnode = files[f]
687 687 if not fnode:
688 688 continue
689 689
690 690 fctx = ctx.filectx(full)
691 691 yield {"file": full,
692 692 "parity": parity.next(),
693 693 "basename": f,
694 694 "date": fctx.changectx().date(),
695 695 "size": fctx.size(),
696 696 "permissions": mf.flags(full)}
697 697
698 698 def dirlist(**map):
699 699 fl = files.keys()
700 700 fl.sort()
701 701 for f in fl:
702 702 full, fnode = files[f]
703 703 if fnode:
704 704 continue
705 705
706 706 yield {"parity": parity.next(),
707 707 "path": "%s%s" % (abspath, f),
708 708 "basename": f[:-1]}
709 709
710 710 yield tmpl("manifest",
711 711 rev=ctx.rev(),
712 712 node=hex(node),
713 713 path=abspath,
714 714 up=_up(abspath),
715 715 upparity=parity.next(),
716 716 fentries=filelist,
717 717 dentries=dirlist,
718 718 archives=self.archivelist(hex(node)),
719 719 tags=self.nodetagsdict(node),
720 720 branches=self.nodebranchdict(ctx))
721 721
722 722 def tags(self, tmpl):
723 723 i = self.repo.tagslist()
724 724 i.reverse()
725 725 parity = paritygen(self.stripecount)
726 726
727 727 def entries(notip=False,limit=0, **map):
728 728 count = 0
729 729 for k, n in i:
730 730 if notip and k == "tip":
731 731 continue
732 732 if limit > 0 and count >= limit:
733 733 continue
734 734 count = count + 1
735 735 yield {"parity": parity.next(),
736 736 "tag": k,
737 737 "date": self.repo.changectx(n).date(),
738 738 "node": hex(n)}
739 739
740 740 yield tmpl("tags",
741 741 node=hex(self.repo.changelog.tip()),
742 742 entries=lambda **x: entries(False,0, **x),
743 743 entriesnotip=lambda **x: entries(True,0, **x),
744 744 latestentry=lambda **x: entries(True,1, **x))
745 745
746 746 def summary(self, tmpl):
747 747 i = self.repo.tagslist()
748 748 i.reverse()
749 749
750 750 def tagentries(**map):
751 751 parity = paritygen(self.stripecount)
752 752 count = 0
753 753 for k, n in i:
754 754 if k == "tip": # skip tip
755 755 continue;
756 756
757 757 count += 1
758 758 if count > 10: # limit to 10 tags
759 759 break;
760 760
761 761 yield tmpl("tagentry",
762 762 parity=parity.next(),
763 763 tag=k,
764 764 node=hex(n),
765 765 date=self.repo.changectx(n).date())
766 766
767 767
768 768 def branches(**map):
769 769 parity = paritygen(self.stripecount)
770 770
771 771 b = self.repo.branchtags()
772 772 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
773 773 l.sort()
774 774
775 775 for r,n,t in l:
776 776 ctx = self.repo.changectx(n)
777 777
778 778 yield {'parity': parity.next(),
779 779 'branch': t,
780 780 'node': hex(n),
781 781 'date': ctx.date()}
782 782
783 783 def changelist(**map):
784 784 parity = paritygen(self.stripecount, offset=start-end)
785 785 l = [] # build a list in forward order for efficiency
786 786 for i in xrange(start, end):
787 787 ctx = self.repo.changectx(i)
788 788 n = ctx.node()
789 789 hn = hex(n)
790 790
791 791 l.insert(0, tmpl(
792 792 'shortlogentry',
793 793 parity=parity.next(),
794 794 author=ctx.user(),
795 795 desc=ctx.description(),
796 796 date=ctx.date(),
797 797 rev=i,
798 798 node=hn,
799 799 tags=self.nodetagsdict(n),
800 800 branches=self.nodebranchdict(ctx)))
801 801
802 802 yield l
803 803
804 804 cl = self.repo.changelog
805 805 count = cl.count()
806 806 start = max(0, count - self.maxchanges)
807 807 end = min(count, start + self.maxchanges)
808 808
809 809 yield tmpl("summary",
810 810 desc=self.config("web", "description", "unknown"),
811 owner=(self.config("ui", "username") or # preferred
812 self.config("web", "contact") or # deprecated
813 self.config("web", "author", "unknown")), # also
811 owner=get_contact(self.config) or "unknown",
814 812 lastchange=cl.read(cl.tip())[2],
815 813 tags=tagentries,
816 814 branches=branches,
817 815 shortlog=changelist,
818 816 node=hex(cl.tip()),
819 817 archives=self.archivelist("tip"))
820 818
821 819 def filediff(self, tmpl, fctx):
822 820 n = fctx.node()
823 821 path = fctx.path()
824 822 parents = fctx.parents()
825 823 p1 = parents and parents[0].node() or nullid
826 824
827 825 def diff(**map):
828 826 yield self.diff(tmpl, p1, n, [path])
829 827
830 828 yield tmpl("filediff",
831 829 file=path,
832 830 node=hex(n),
833 831 rev=fctx.rev(),
834 832 parent=self.siblings(parents),
835 833 child=self.siblings(fctx.children()),
836 834 diff=diff)
837 835
838 836 archive_specs = {
839 837 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
840 838 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
841 839 'zip': ('application/zip', 'zip', '.zip', None),
842 840 }
843 841
844 842 def archive(self, tmpl, req, key, type_):
845 843 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
846 844 cnode = self.repo.lookup(key)
847 845 arch_version = key
848 846 if cnode == key or key == 'tip':
849 847 arch_version = short(cnode)
850 848 name = "%s-%s" % (reponame, arch_version)
851 849 mimetype, artype, extension, encoding = self.archive_specs[type_]
852 850 headers = [('Content-type', mimetype),
853 851 ('Content-disposition', 'attachment; filename=%s%s' %
854 852 (name, extension))]
855 853 if encoding:
856 854 headers.append(('Content-encoding', encoding))
857 855 req.header(headers)
858 856 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
859 857
860 858 # add tags to things
861 859 # tags -> list of changesets corresponding to tags
862 860 # find tag, changeset, file
863 861
864 862 def cleanpath(self, path):
865 863 path = path.lstrip('/')
866 864 return util.canonpath(self.repo.root, '', path)
867 865
868 866 def changectx(self, req):
869 867 if req.form.has_key('node'):
870 868 changeid = req.form['node'][0]
871 869 elif req.form.has_key('manifest'):
872 870 changeid = req.form['manifest'][0]
873 871 else:
874 872 changeid = self.repo.changelog.count() - 1
875 873
876 874 try:
877 875 ctx = self.repo.changectx(changeid)
878 876 except hg.RepoError:
879 877 man = self.repo.manifest
880 878 mn = man.lookup(changeid)
881 879 ctx = self.repo.changectx(man.linkrev(mn))
882 880
883 881 return ctx
884 882
885 883 def filectx(self, req):
886 884 path = self.cleanpath(req.form['file'][0])
887 885 if req.form.has_key('node'):
888 886 changeid = req.form['node'][0]
889 887 else:
890 888 changeid = req.form['filenode'][0]
891 889 try:
892 890 ctx = self.repo.changectx(changeid)
893 891 fctx = ctx.filectx(path)
894 892 except hg.RepoError:
895 893 fctx = self.repo.filectx(path, fileid=changeid)
896 894
897 895 return fctx
898 896
899 897 def check_perm(self, req, op, default):
900 898 '''check permission for operation based on user auth.
901 899 return true if op allowed, else false.
902 900 default is policy to use if no config given.'''
903 901
904 902 user = req.env.get('REMOTE_USER')
905 903
906 904 deny = self.configlist('web', 'deny_' + op)
907 905 if deny and (not user or deny == ['*'] or user in deny):
908 906 return False
909 907
910 908 allow = self.configlist('web', 'allow_' + op)
911 909 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,277 +1,276 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetools, cStringIO
10 10 from mercurial.i18n import gettext as _
11 11 from mercurial import ui, hg, util, templater
12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen
12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen, \
13 get_contact
13 14 from hgweb_mod import hgweb
14 15 from request import wsgirequest
15 16
16 17 # This is a stopgap
17 18 class hgwebdir(object):
18 19 def __init__(self, config, parentui=None):
19 20 def cleannames(items):
20 21 return [(util.pconvert(name).strip('/'), path)
21 22 for name, path in items]
22 23
23 24 self.parentui = parentui or ui.ui(report_untrusted=False,
24 25 interactive = False)
25 26 self.motd = None
26 27 self.style = None
27 28 self.stripecount = None
28 29 self.repos_sorted = ('name', False)
29 30 if isinstance(config, (list, tuple)):
30 31 self.repos = cleannames(config)
31 32 self.repos_sorted = ('', False)
32 33 elif isinstance(config, dict):
33 34 self.repos = cleannames(config.items())
34 35 self.repos.sort()
35 36 else:
36 37 if isinstance(config, util.configparser):
37 38 cp = config
38 39 else:
39 40 cp = util.configparser()
40 41 cp.read(config)
41 42 self.repos = []
42 43 if cp.has_section('web'):
43 44 if cp.has_option('web', 'motd'):
44 45 self.motd = cp.get('web', 'motd')
45 46 if cp.has_option('web', 'style'):
46 47 self.style = cp.get('web', 'style')
47 48 if cp.has_option('web', 'stripes'):
48 49 self.stripecount = int(cp.get('web', 'stripes'))
49 50 if cp.has_section('paths'):
50 51 self.repos.extend(cleannames(cp.items('paths')))
51 52 if cp.has_section('collections'):
52 53 for prefix, root in cp.items('collections'):
53 54 for path in util.walkrepos(root):
54 55 repo = os.path.normpath(path)
55 56 name = repo
56 57 if name.startswith(prefix):
57 58 name = name[len(prefix):]
58 59 self.repos.append((name.lstrip(os.sep), repo))
59 60 self.repos.sort()
60 61
61 62 def run(self):
62 63 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
63 64 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
64 65 import mercurial.hgweb.wsgicgi as wsgicgi
65 66 wsgicgi.launch(self)
66 67
67 68 def __call__(self, env, respond):
68 69 req = wsgirequest(env, respond)
69 70 self.run_wsgi(req)
70 71 return req
71 72
72 73 def run_wsgi(self, req):
73 74
74 75 try:
75 76 try:
76 77
77 78 virtual = req.env.get("PATH_INFO", "").strip('/')
78 79
79 80 # a static file
80 81 if virtual.startswith('static/') or 'static' in req.form:
81 82 static = os.path.join(templater.templatepath(), 'static')
82 83 if virtual.startswith('static/'):
83 84 fname = virtual[7:]
84 85 else:
85 86 fname = req.form['static'][0]
86 87 req.write(staticfile(static, fname, req))
87 88 return
88 89
89 90 # top-level index
90 91 elif not virtual:
91 92 tmpl = self.templater(req)
92 93 self.makeindex(req, tmpl)
93 94 return
94 95
95 96 # nested indexes and hgwebs
96 97 repos = dict(self.repos)
97 98 while virtual:
98 99 real = repos.get(virtual)
99 100 if real:
100 101 req.env['REPO_NAME'] = virtual
101 102 try:
102 103 repo = hg.repository(self.parentui, real)
103 104 hgweb(repo).run_wsgi(req)
104 105 return
105 106 except IOError, inst:
106 107 raise ErrorResponse(500, inst.strerror)
107 108 except hg.RepoError, inst:
108 109 raise ErrorResponse(500, str(inst))
109 110
110 111 # browse subdirectories
111 112 subdir = virtual + '/'
112 113 if [r for r in repos if r.startswith(subdir)]:
113 114 tmpl = self.templater(req)
114 115 self.makeindex(req, tmpl, subdir)
115 116 return
116 117
117 118 up = virtual.rfind('/')
118 119 if up < 0:
119 120 break
120 121 virtual = virtual[:up]
121 122
122 123 # prefixes not found
123 124 tmpl = self.templater(req)
124 125 req.respond(404, tmpl("notfound", repo=virtual))
125 126
126 127 except ErrorResponse, err:
127 128 tmpl = self.templater(req)
128 129 req.respond(err.code, tmpl('error', error=err.message or ''))
129 130 finally:
130 131 tmpl = None
131 132
132 133 def makeindex(self, req, tmpl, subdir=""):
133 134
134 135 def archivelist(ui, nodeid, url):
135 136 allowed = ui.configlist("web", "allow_archive", untrusted=True)
136 137 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
137 138 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
138 139 untrusted=True):
139 140 yield {"type" : i[0], "extension": i[1],
140 141 "node": nodeid, "url": url}
141 142
142 143 def entries(sortcolumn="", descending=False, subdir="", **map):
143 144 def sessionvars(**map):
144 145 fields = []
145 146 if req.form.has_key('style'):
146 147 style = req.form['style'][0]
147 148 if style != get('web', 'style', ''):
148 149 fields.append(('style', style))
149 150
150 151 separator = url[-1] == '?' and ';' or '?'
151 152 for name, value in fields:
152 153 yield dict(name=name, value=value, separator=separator)
153 154 separator = ';'
154 155
155 156 rows = []
156 157 parity = paritygen(self.stripecount)
157 158 for name, path in self.repos:
158 159 if not name.startswith(subdir):
159 160 continue
160 161 name = name[len(subdir):]
161 162
162 163 u = ui.ui(parentui=self.parentui)
163 164 try:
164 165 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
165 166 except Exception, e:
166 167 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
167 168 continue
168 169 def get(section, name, default=None):
169 170 return u.config(section, name, default, untrusted=True)
170 171
171 172 if u.configbool("web", "hidden", untrusted=True):
172 173 continue
173 174
174 175 parts = [req.env['PATH_INFO'], name]
175 176 if req.env['SCRIPT_NAME']:
176 177 parts.insert(0, req.env['SCRIPT_NAME'])
177 178 url = ('/'.join(parts).replace("//", "/")) + '/'
178 179
179 180 # update time with local timezone
180 181 try:
181 182 d = (get_mtime(path), util.makedate()[1])
182 183 except OSError:
183 184 continue
184 185
185 contact = (get("ui", "username") or # preferred
186 get("web", "contact") or # deprecated
187 get("web", "author", "")) # also
186 contact = get_contact(get)
188 187 description = get("web", "description", "")
189 188 name = get("web", "name", name)
190 189 row = dict(contact=contact or "unknown",
191 190 contact_sort=contact.upper() or "unknown",
192 191 name=name,
193 192 name_sort=name,
194 193 url=url,
195 194 description=description or "unknown",
196 195 description_sort=description.upper() or "unknown",
197 196 lastchange=d,
198 197 lastchange_sort=d[1]-d[0],
199 198 sessionvars=sessionvars,
200 199 archives=archivelist(u, "tip", url))
201 200 if (not sortcolumn
202 201 or (sortcolumn, descending) == self.repos_sorted):
203 202 # fast path for unsorted output
204 203 row['parity'] = parity.next()
205 204 yield row
206 205 else:
207 206 rows.append((row["%s_sort" % sortcolumn], row))
208 207 if rows:
209 208 rows.sort()
210 209 if descending:
211 210 rows.reverse()
212 211 for key, row in rows:
213 212 row['parity'] = parity.next()
214 213 yield row
215 214
216 215 sortable = ["name", "description", "contact", "lastchange"]
217 216 sortcolumn, descending = self.repos_sorted
218 217 if req.form.has_key('sort'):
219 218 sortcolumn = req.form['sort'][0]
220 219 descending = sortcolumn.startswith('-')
221 220 if descending:
222 221 sortcolumn = sortcolumn[1:]
223 222 if sortcolumn not in sortable:
224 223 sortcolumn = ""
225 224
226 225 sort = [("sort_%s" % column,
227 226 "%s%s" % ((not descending and column == sortcolumn)
228 227 and "-" or "", column))
229 228 for column in sortable]
230 229 req.write(tmpl("index", entries=entries, subdir=subdir,
231 230 sortcolumn=sortcolumn, descending=descending,
232 231 **dict(sort)))
233 232
234 233 def templater(self, req):
235 234
236 235 def header(**map):
237 236 header_file = cStringIO.StringIO(
238 237 ''.join(tmpl("header", encoding=util._encoding, **map)))
239 238 msg = mimetools.Message(header_file, 0)
240 239 req.header(msg.items())
241 240 yield header_file.read()
242 241
243 242 def footer(**map):
244 243 yield tmpl("footer", **map)
245 244
246 245 def motd(**map):
247 246 if self.motd is not None:
248 247 yield self.motd
249 248 else:
250 249 yield config('web', 'motd', '')
251 250
252 251 def config(section, name, default=None, untrusted=True):
253 252 return self.parentui.config(section, name, default, untrusted)
254 253
255 254 url = req.env.get('SCRIPT_NAME', '')
256 255 if not url.endswith('/'):
257 256 url += '/'
258 257
259 258 staticurl = config('web', 'staticurl') or url + 'static/'
260 259 if not staticurl.endswith('/'):
261 260 staticurl += '/'
262 261
263 262 style = self.style
264 263 if style is None:
265 264 style = config('web', 'style', '')
266 265 if req.form.has_key('style'):
267 266 style = req.form['style'][0]
268 267 if self.stripecount is None:
269 268 self.stripecount = int(config('web', 'stripes', 1))
270 269 mapfile = style_map(templater.templatepath(), style)
271 270 tmpl = templater.templater(mapfile, templater.common_filters,
272 271 defaults={"header": header,
273 272 "footer": footer,
274 273 "motd": motd,
275 274 "url": url,
276 275 "staticurl": staticurl})
277 276 return tmpl
@@ -1,582 +1,583 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9
10 10 import difflib
11 11 import errno
12 12 import optparse
13 13 import os
14 14 import popen2
15 15 import re
16 16 import shutil
17 17 import signal
18 18 import sys
19 19 import tempfile
20 20 import time
21 21
22 22 # reserved exit code to skip test (used by hghave)
23 23 SKIPPED_STATUS = 80
24 24 SKIPPED_PREFIX = 'skipped: '
25 25
26 26 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
27 27
28 28 parser = optparse.OptionParser("%prog [options] [tests]")
29 29 parser.add_option("-C", "--annotate", action="store_true",
30 30 help="output files annotated with coverage")
31 31 parser.add_option("--child", type="int",
32 32 help="run as child process, summary to given fd")
33 33 parser.add_option("-c", "--cover", action="store_true",
34 34 help="print a test coverage report")
35 35 parser.add_option("-f", "--first", action="store_true",
36 36 help="exit on the first test failure")
37 37 parser.add_option("-i", "--interactive", action="store_true",
38 38 help="prompt to accept changed output")
39 39 parser.add_option("-j", "--jobs", type="int",
40 40 help="number of jobs to run in parallel")
41 41 parser.add_option("-R", "--restart", action="store_true",
42 42 help="restart at last error")
43 43 parser.add_option("-p", "--port", type="int",
44 44 help="port on which servers should listen")
45 45 parser.add_option("-r", "--retest", action="store_true",
46 46 help="retest failed tests")
47 47 parser.add_option("-s", "--cover_stdlib", action="store_true",
48 48 help="print a test coverage report inc. standard libraries")
49 49 parser.add_option("-t", "--timeout", type="int",
50 50 help="kill errant tests after TIMEOUT seconds")
51 51 parser.add_option("--tmpdir", type="string",
52 52 help="run tests in the given temporary directory")
53 53 parser.add_option("-v", "--verbose", action="store_true",
54 54 help="output verbose messages")
55 55 parser.add_option("--with-hg", type="string",
56 56 help="test existing install at given location")
57 57
58 58 parser.set_defaults(jobs=1, port=20059, timeout=180)
59 59 (options, args) = parser.parse_args()
60 60 verbose = options.verbose
61 61 coverage = options.cover or options.cover_stdlib or options.annotate
62 62 python = sys.executable
63 63
64 64 if options.jobs < 1:
65 65 print >> sys.stderr, 'ERROR: -j/--jobs must be positive'
66 66 sys.exit(1)
67 67 if options.interactive and options.jobs > 1:
68 68 print >> sys.stderr, 'ERROR: cannot mix -interactive and --jobs > 1'
69 69 sys.exit(1)
70 70
71 71 def vlog(*msg):
72 72 if verbose:
73 73 for m in msg:
74 74 print m,
75 75 print
76 76
77 77 def splitnewlines(text):
78 78 '''like str.splitlines, but only split on newlines.
79 79 keep line endings.'''
80 80 i = 0
81 81 lines = []
82 82 while True:
83 83 n = text.find('\n', i)
84 84 if n == -1:
85 85 last = text[i:]
86 86 if last:
87 87 lines.append(last)
88 88 return lines
89 89 lines.append(text[i:n+1])
90 90 i = n + 1
91 91
92 92 def extract_missing_features(lines):
93 93 '''Extract missing/unknown features log lines as a list'''
94 94 missing = []
95 95 for line in lines:
96 96 if not line.startswith(SKIPPED_PREFIX):
97 97 continue
98 98 line = line.splitlines()[0]
99 99 missing.append(line[len(SKIPPED_PREFIX):])
100 100
101 101 return missing
102 102
103 103 def show_diff(expected, output):
104 104 for line in difflib.unified_diff(expected, output,
105 105 "Expected output", "Test output"):
106 106 sys.stdout.write(line)
107 107
108 108 def find_program(program):
109 109 """Search PATH for a executable program"""
110 110 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
111 111 name = os.path.join(p, program)
112 112 if os.access(name, os.X_OK):
113 113 return name
114 114 return None
115 115
116 116 def check_required_tools():
117 117 # Before we go any further, check for pre-requisite tools
118 118 # stuff from coreutils (cat, rm, etc) are not tested
119 119 for p in required_tools:
120 120 if os.name == 'nt':
121 121 p += '.exe'
122 122 found = find_program(p)
123 123 if found:
124 124 vlog("# Found prerequisite", p, "at", found)
125 125 else:
126 126 print "WARNING: Did not find prerequisite tool: "+p
127 127
128 128 def cleanup_exit():
129 129 if verbose:
130 130 print "# Cleaning up HGTMP", HGTMP
131 131 shutil.rmtree(HGTMP, True)
132 132
133 133 def use_correct_python():
134 134 # some tests run python interpreter. they must use same
135 135 # interpreter we use or bad things will happen.
136 136 exedir, exename = os.path.split(sys.executable)
137 137 if exename == 'python':
138 138 path = find_program('python')
139 139 if os.path.dirname(path) == exedir:
140 140 return
141 141 vlog('# Making python executable in test path use correct Python')
142 142 my_python = os.path.join(BINDIR, 'python')
143 143 try:
144 144 os.symlink(sys.executable, my_python)
145 145 except AttributeError:
146 146 # windows fallback
147 147 shutil.copyfile(sys.executable, my_python)
148 148 shutil.copymode(sys.executable, my_python)
149 149
150 150 def install_hg():
151 151 global python
152 152 vlog("# Performing temporary installation of HG")
153 153 installerrs = os.path.join("tests", "install.err")
154 154
155 155 # Run installer in hg root
156 156 os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..'))
157 157 cmd = ('%s setup.py clean --all'
158 158 ' install --force --home="%s" --install-lib="%s"'
159 159 ' --install-scripts="%s" >%s 2>&1'
160 160 % (sys.executable, INST, PYTHONDIR, BINDIR, installerrs))
161 161 vlog("# Running", cmd)
162 162 if os.system(cmd) == 0:
163 163 if not verbose:
164 164 os.remove(installerrs)
165 165 else:
166 166 f = open(installerrs)
167 167 for line in f:
168 168 print line,
169 169 f.close()
170 170 sys.exit(1)
171 171 os.chdir(TESTDIR)
172 172
173 173 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
174 174
175 175 pydir = os.pathsep.join([PYTHONDIR, TESTDIR])
176 176 pythonpath = os.environ.get("PYTHONPATH")
177 177 if pythonpath:
178 178 pythonpath = pydir + os.pathsep + pythonpath
179 179 else:
180 180 pythonpath = pydir
181 181 os.environ["PYTHONPATH"] = pythonpath
182 182
183 183 use_correct_python()
184 184
185 185 if coverage:
186 186 vlog("# Installing coverage wrapper")
187 187 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
188 188 if os.path.exists(COVERAGE_FILE):
189 189 os.unlink(COVERAGE_FILE)
190 190 # Create a wrapper script to invoke hg via coverage.py
191 191 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
192 192 f = open(os.path.join(BINDIR, 'hg'), 'w')
193 193 f.write('#!' + sys.executable + '\n')
194 194 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
195 195 '"%s", "-x", "%s"] + sys.argv[1:])\n' %
196 196 (os.path.join(TESTDIR, 'coverage.py'),
197 197 os.path.join(BINDIR, '_hg.py')))
198 198 f.close()
199 199 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
200 200 python = '"%s" "%s" -x' % (sys.executable,
201 201 os.path.join(TESTDIR,'coverage.py'))
202 202
203 203 def output_coverage():
204 204 vlog("# Producing coverage report")
205 205 omit = [BINDIR, TESTDIR, PYTHONDIR]
206 206 if not options.cover_stdlib:
207 207 # Exclude as system paths (ignoring empty strings seen on win)
208 208 omit += [x for x in sys.path if x != '']
209 209 omit = ','.join(omit)
210 210 os.chdir(PYTHONDIR)
211 211 cmd = '"%s" "%s" -i -r "--omit=%s"' % (
212 212 sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
213 213 vlog("# Running: "+cmd)
214 214 os.system(cmd)
215 215 if options.annotate:
216 216 adir = os.path.join(TESTDIR, 'annotated')
217 217 if not os.path.isdir(adir):
218 218 os.mkdir(adir)
219 219 cmd = '"%s" "%s" -i -a "--directory=%s" "--omit=%s"' % (
220 220 sys.executable, os.path.join(TESTDIR, 'coverage.py'),
221 221 adir, omit)
222 222 vlog("# Running: "+cmd)
223 223 os.system(cmd)
224 224
225 225 class Timeout(Exception):
226 226 pass
227 227
228 228 def alarmed(signum, frame):
229 229 raise Timeout
230 230
231 231 def run(cmd):
232 232 """Run command in a sub-process, capturing the output (stdout and stderr).
233 233 Return the exist code, and output."""
234 234 # TODO: Use subprocess.Popen if we're running on Python 2.4
235 235 if os.name == 'nt':
236 236 tochild, fromchild = os.popen4(cmd)
237 237 tochild.close()
238 238 output = fromchild.read()
239 239 ret = fromchild.close()
240 240 if ret == None:
241 241 ret = 0
242 242 else:
243 243 proc = popen2.Popen4(cmd)
244 244 try:
245 245 output = ''
246 246 proc.tochild.close()
247 247 output = proc.fromchild.read()
248 248 ret = proc.wait()
249 249 if os.WIFEXITED(ret):
250 250 ret = os.WEXITSTATUS(ret)
251 251 except Timeout:
252 252 vlog('# Process %d timed out - killing it' % proc.pid)
253 253 os.kill(proc.pid, signal.SIGTERM)
254 254 ret = proc.wait()
255 255 if ret == 0:
256 256 ret = signal.SIGTERM << 8
257 257 output += ("\n### Abort: timeout after %d seconds.\n"
258 258 % options.timeout)
259 259 return ret, splitnewlines(output)
260 260
261 261 def run_one(test, skips):
262 262 '''tristate output:
263 263 None -> skipped
264 264 True -> passed
265 265 False -> failed'''
266 266
267 267 def skip(msg):
268 268 if not verbose:
269 269 skips.append((test, msg))
270 270 else:
271 271 print "\nSkipping %s: %s" % (test, msg)
272 272 return None
273 273
274 274 vlog("# Test", test)
275 275
276 276 # create a fresh hgrc
277 277 hgrc = file(HGRCPATH, 'w+')
278 278 hgrc.write('[ui]\n')
279 279 hgrc.write('slash = True\n')
280 280 hgrc.write('[defaults]\n')
281 281 hgrc.write('backout = -d "0 0"\n')
282 282 hgrc.write('commit = -d "0 0"\n')
283 283 hgrc.write('debugrawcommit = -d "0 0"\n')
284 284 hgrc.write('tag = -d "0 0"\n')
285 285 hgrc.close()
286 286
287 287 err = os.path.join(TESTDIR, test+".err")
288 288 ref = os.path.join(TESTDIR, test+".out")
289 289 testpath = os.path.join(TESTDIR, test)
290 290
291 291 if os.path.exists(err):
292 292 os.remove(err) # Remove any previous output files
293 293
294 294 # Make a tmp subdirectory to work in
295 295 tmpd = os.path.join(HGTMP, test)
296 296 os.mkdir(tmpd)
297 297 os.chdir(tmpd)
298 298
299 299 try:
300 300 tf = open(testpath)
301 301 firstline = tf.readline().rstrip()
302 302 tf.close()
303 303 except:
304 304 firstline = ''
305 305 lctest = test.lower()
306 306
307 307 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
308 308 cmd = '%s "%s"' % (python, testpath)
309 309 elif lctest.endswith('.bat'):
310 310 # do not run batch scripts on non-windows
311 311 if os.name != 'nt':
312 312 return skip("batch script")
313 313 # To reliably get the error code from batch files on WinXP,
314 314 # the "cmd /c call" prefix is needed. Grrr
315 315 cmd = 'cmd /c call "%s"' % testpath
316 316 else:
317 317 # do not run shell scripts on windows
318 318 if os.name == 'nt':
319 319 return skip("shell script")
320 320 # do not try to run non-executable programs
321 321 if not os.access(testpath, os.X_OK):
322 322 return skip("not executable")
323 323 cmd = '"%s"' % testpath
324 324
325 325 if options.timeout > 0:
326 326 signal.alarm(options.timeout)
327 327
328 328 vlog("# Running", cmd)
329 329 ret, out = run(cmd)
330 330 vlog("# Ret was:", ret)
331 331
332 332 if options.timeout > 0:
333 333 signal.alarm(0)
334 334
335 335 skipped = (ret == SKIPPED_STATUS)
336 336 diffret = 0
337 337 # If reference output file exists, check test output against it
338 338 if os.path.exists(ref):
339 339 f = open(ref, "r")
340 340 ref_out = splitnewlines(f.read())
341 341 f.close()
342 342 else:
343 343 ref_out = []
344 344 if not skipped and out != ref_out:
345 345 diffret = 1
346 346 print "\nERROR: %s output changed" % (test)
347 347 show_diff(ref_out, out)
348 348 if skipped:
349 349 missing = extract_missing_features(out)
350 350 if not missing:
351 351 missing = ['irrelevant']
352 352 skip(missing[-1])
353 353 elif ret:
354 354 print "\nERROR: %s failed with error code %d" % (test, ret)
355 355 elif diffret:
356 356 ret = diffret
357 357
358 358 if not verbose:
359 359 sys.stdout.write(skipped and 's' or '.')
360 360 sys.stdout.flush()
361 361
362 362 if ret != 0 and not skipped:
363 363 # Save errors to a file for diagnosis
364 364 f = open(err, "wb")
365 365 for line in out:
366 366 f.write(line)
367 367 f.close()
368 368
369 369 # Kill off any leftover daemon processes
370 370 try:
371 371 fp = file(DAEMON_PIDS)
372 372 for line in fp:
373 373 try:
374 374 pid = int(line)
375 375 except ValueError:
376 376 continue
377 377 try:
378 378 os.kill(pid, 0)
379 379 vlog('# Killing daemon process %d' % pid)
380 380 os.kill(pid, signal.SIGTERM)
381 381 time.sleep(0.25)
382 382 os.kill(pid, 0)
383 383 vlog('# Daemon process %d is stuck - really killing it' % pid)
384 384 os.kill(pid, signal.SIGKILL)
385 385 except OSError, err:
386 386 if err.errno != errno.ESRCH:
387 387 raise
388 388 fp.close()
389 389 os.unlink(DAEMON_PIDS)
390 390 except IOError:
391 391 pass
392 392
393 393 os.chdir(TESTDIR)
394 394 shutil.rmtree(tmpd, True)
395 395 if skipped:
396 396 return None
397 397 return ret == 0
398 398
399 399 if not options.child:
400 400 os.umask(022)
401 401
402 402 check_required_tools()
403 403
404 404 # Reset some environment variables to well-known values so that
405 405 # the tests produce repeatable output.
406 406 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
407 407 os.environ['TZ'] = 'GMT'
408 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
408 409
409 410 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
410 411 HGTMP = os.environ['HGTMP'] = tempfile.mkdtemp('', 'hgtests.', options.tmpdir)
411 412 DAEMON_PIDS = None
412 413 HGRCPATH = None
413 414
414 415 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
415 416 os.environ["HGMERGE"] = ('python "%s" -L my -L other'
416 417 % os.path.join(TESTDIR, os.path.pardir,
417 418 'contrib', 'simplemerge'))
418 419 os.environ["HGUSER"] = "test"
419 420 os.environ["HGENCODING"] = "ascii"
420 421 os.environ["HGENCODINGMODE"] = "strict"
421 422 os.environ["HGPORT"] = str(options.port)
422 423 os.environ["HGPORT1"] = str(options.port + 1)
423 424 os.environ["HGPORT2"] = str(options.port + 2)
424 425
425 426 if options.with_hg:
426 427 INST = options.with_hg
427 428 else:
428 429 INST = os.path.join(HGTMP, "install")
429 430 BINDIR = os.path.join(INST, "bin")
430 431 PYTHONDIR = os.path.join(INST, "lib", "python")
431 432 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
432 433
433 434 def run_children(tests):
434 435 if not options.with_hg:
435 436 install_hg()
436 437
437 438 optcopy = dict(options.__dict__)
438 439 optcopy['jobs'] = 1
439 440 optcopy['with_hg'] = INST
440 441 opts = []
441 442 for opt, value in optcopy.iteritems():
442 443 name = '--' + opt.replace('_', '-')
443 444 if value is True:
444 445 opts.append(name)
445 446 elif value is not None:
446 447 opts.append(name + '=' + str(value))
447 448
448 449 tests.reverse()
449 450 jobs = [[] for j in xrange(options.jobs)]
450 451 while tests:
451 452 for j in xrange(options.jobs):
452 453 if not tests: break
453 454 jobs[j].append(tests.pop())
454 455 fps = {}
455 456 for j in xrange(len(jobs)):
456 457 job = jobs[j]
457 458 if not job:
458 459 continue
459 460 rfd, wfd = os.pipe()
460 461 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
461 462 cmdline = [python, sys.argv[0]] + opts + childopts + job
462 463 vlog(' '.join(cmdline))
463 464 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
464 465 os.close(wfd)
465 466 failures = 0
466 467 tested, skipped, failed = 0, 0, 0
467 468 skips = []
468 469 while fps:
469 470 pid, status = os.wait()
470 471 fp = fps.pop(pid)
471 472 l = fp.read().splitlines()
472 473 test, skip, fail = map(int, l[:3])
473 474 for s in l[3:]:
474 475 skips.append(s.split(" ", 1))
475 476 tested += test
476 477 skipped += skip
477 478 failed += fail
478 479 vlog('pid %d exited, status %d' % (pid, status))
479 480 failures |= status
480 481 print
481 482 for s in skips:
482 483 print "Skipped %s: %s" % (s[0], s[1])
483 484 print "# Ran %d tests, %d skipped, %d failed." % (
484 485 tested, skipped, failed)
485 486 sys.exit(failures != 0)
486 487
487 488 def run_tests(tests):
488 489 global DAEMON_PIDS, HGRCPATH
489 490 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
490 491 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
491 492
492 493 try:
493 494 if not options.with_hg:
494 495 install_hg()
495 496
496 497 if options.timeout > 0:
497 498 try:
498 499 signal.signal(signal.SIGALRM, alarmed)
499 500 vlog('# Running tests with %d-second timeout' %
500 501 options.timeout)
501 502 except AttributeError:
502 503 print 'WARNING: cannot run tests with timeouts'
503 504 options.timeout = 0
504 505
505 506 tested = 0
506 507 failed = 0
507 508 skipped = 0
508 509
509 510 if options.restart:
510 511 orig = list(tests)
511 512 while tests:
512 513 if os.path.exists(tests[0] + ".err"):
513 514 break
514 515 tests.pop(0)
515 516 if not tests:
516 517 print "running all tests"
517 518 tests = orig
518 519
519 520 skips = []
520 521 for test in tests:
521 522 if options.retest and not os.path.exists(test + ".err"):
522 523 skipped += 1
523 524 continue
524 525 ret = run_one(test, skips)
525 526 if ret is None:
526 527 skipped += 1
527 528 elif not ret:
528 529 if options.interactive:
529 530 print "Accept this change? [n] ",
530 531 answer = sys.stdin.readline().strip()
531 532 if answer.lower() in "y yes".split():
532 533 os.rename(test + ".err", test + ".out")
533 534 tested += 1
534 535 continue
535 536 failed += 1
536 537 if options.first:
537 538 break
538 539 tested += 1
539 540
540 541 if options.child:
541 542 fp = os.fdopen(options.child, 'w')
542 543 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
543 544 for s in skips:
544 545 fp.write("%s %s\n" % s)
545 546 fp.close()
546 547 else:
547 548 print
548 549 for s in skips:
549 550 print "Skipped %s: %s" % s
550 551 print "# Ran %d tests, %d skipped, %d failed." % (
551 552 tested, skipped, failed)
552 553
553 554 if coverage:
554 555 output_coverage()
555 556 except KeyboardInterrupt:
556 557 failed = True
557 558 print "\ninterrupted!"
558 559
559 560 if failed:
560 561 sys.exit(1)
561 562
562 563 if len(args) == 0:
563 564 args = os.listdir(".")
564 565 args.sort()
565 566
566 567 tests = []
567 568 for test in args:
568 569 if (test.startswith("test-") and '~' not in test and
569 570 ('.' not in test or test.endswith('.py') or
570 571 test.endswith('.bat'))):
571 572 tests.append(test)
572 573
573 574 vlog("# Using TESTDIR", TESTDIR)
574 575 vlog("# Using HGTMP", HGTMP)
575 576
576 577 try:
577 578 if len(tests) > 1 and options.jobs > 1:
578 579 run_children(tests)
579 580 else:
580 581 run_tests(tests)
581 582 finally:
582 583 cleanup_exit()
1 NO CONTENT: modified file, binary diff hidden
General Comments 0
You need to be logged in to leave comments. Login now