##// END OF EJS Templates
initial commit
johbo -
r0:0fb8cb8f default
parent child Browse files
Show More
@@ -0,0 +1,50 b''
1 syntax: glob
2 *.orig
3 *.pyc
4 *.swp
5 *.sqlite
6 *.sqlite-journal
7 *.tox
8 *.egg-info
9 *.egg
10 *.idea
11 .DS_Store*
12
13
14 syntax: regexp
15
16 #.filename
17 ^\.settings$
18 ^\.project$
19 ^\.pydevproject$
20 ^\.coverage$
21 ^\.cache.*$
22 ^\.rhodecode$
23
24 ^rcextensions
25 ^_dev
26 ^._dev
27 ^build/
28 ^coverage\.xml$
29 ^data$
30 ^dev.ini$
31 ^acceptance_tests/dev.*\.ini$
32 ^dist/
33 ^fabfile.py
34 ^htmlcov
35 ^junit\.xml$
36 ^node_modules/
37 ^pylint.log$
38 ^rcextensions/
39 ^rhodecode/public/css/style.css$
40 ^rhodecode/public/js/scripts.js$
41 ^rhodecode\.db$
42 ^rhodecode\.log$
43 ^rhodecode_dev\.log$
44 ^test\.db$
45 ^build$
46 ^coverage\.xml$
47 ^htmlcov
48 ^junit\.xml$
49 ^pylint.log$
50 ^result$
This diff has been collapsed as it changes many lines, (674 lines changed) Show them Hide them
@@ -0,0 +1,674 b''
1 GNU GENERAL PUBLIC LICENSE
2 Version 3, 29 June 2007
3
4 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5 Everyone is permitted to copy and distribute verbatim copies
6 of this license document, but changing it is not allowed.
7
8 Preamble
9
10 The GNU General Public License is a free, copyleft license for
11 software and other kinds of works.
12
13 The licenses for most software and other practical works are designed
14 to take away your freedom to share and change the works. By contrast,
15 the GNU General Public License is intended to guarantee your freedom to
16 share and change all versions of a program--to make sure it remains free
17 software for all its users. We, the Free Software Foundation, use the
18 GNU General Public License for most of our software; it applies also to
19 any other work released this way by its authors. You can apply it to
20 your programs, too.
21
22 When we speak of free software, we are referring to freedom, not
23 price. Our General Public Licenses are designed to make sure that you
24 have the freedom to distribute copies of free software (and charge for
25 them if you wish), that you receive source code or can get it if you
26 want it, that you can change the software or use pieces of it in new
27 free programs, and that you know you can do these things.
28
29 To protect your rights, we need to prevent others from denying you
30 these rights or asking you to surrender the rights. Therefore, you have
31 certain responsibilities if you distribute copies of the software, or if
32 you modify it: responsibilities to respect the freedom of others.
33
34 For example, if you distribute copies of such a program, whether
35 gratis or for a fee, you must pass on to the recipients the same
36 freedoms that you received. You must make sure that they, too, receive
37 or can get the source code. And you must show them these terms so they
38 know their rights.
39
40 Developers that use the GNU GPL protect your rights with two steps:
41 (1) assert copyright on the software, and (2) offer you this License
42 giving you legal permission to copy, distribute and/or modify it.
43
44 For the developers' and authors' protection, the GPL clearly explains
45 that there is no warranty for this free software. For both users' and
46 authors' sake, the GPL requires that modified versions be marked as
47 changed, so that their problems will not be attributed erroneously to
48 authors of previous versions.
49
50 Some devices are designed to deny users access to install or run
51 modified versions of the software inside them, although the manufacturer
52 can do so. This is fundamentally incompatible with the aim of
53 protecting users' freedom to change the software. The systematic
54 pattern of such abuse occurs in the area of products for individuals to
55 use, which is precisely where it is most unacceptable. Therefore, we
56 have designed this version of the GPL to prohibit the practice for those
57 products. If such problems arise substantially in other domains, we
58 stand ready to extend this provision to those domains in future versions
59 of the GPL, as needed to protect the freedom of users.
60
61 Finally, every program is threatened constantly by software patents.
62 States should not allow patents to restrict development and use of
63 software on general-purpose computers, but in those that do, we wish to
64 avoid the special danger that patents applied to a free program could
65 make it effectively proprietary. To prevent this, the GPL assures that
66 patents cannot be used to render the program non-free.
67
68 The precise terms and conditions for copying, distribution and
69 modification follow.
70
71 TERMS AND CONDITIONS
72
73 0. Definitions.
74
75 "This License" refers to version 3 of the GNU General Public License.
76
77 "Copyright" also means copyright-like laws that apply to other kinds of
78 works, such as semiconductor masks.
79
80 "The Program" refers to any copyrightable work licensed under this
81 License. Each licensee is addressed as "you". "Licensees" and
82 "recipients" may be individuals or organizations.
83
84 To "modify" a work means to copy from or adapt all or part of the work
85 in a fashion requiring copyright permission, other than the making of an
86 exact copy. The resulting work is called a "modified version" of the
87 earlier work or a work "based on" the earlier work.
88
89 A "covered work" means either the unmodified Program or a work based
90 on the Program.
91
92 To "propagate" a work means to do anything with it that, without
93 permission, would make you directly or secondarily liable for
94 infringement under applicable copyright law, except executing it on a
95 computer or modifying a private copy. Propagation includes copying,
96 distribution (with or without modification), making available to the
97 public, and in some countries other activities as well.
98
99 To "convey" a work means any kind of propagation that enables other
100 parties to make or receive copies. Mere interaction with a user through
101 a computer network, with no transfer of a copy, is not conveying.
102
103 An interactive user interface displays "Appropriate Legal Notices"
104 to the extent that it includes a convenient and prominently visible
105 feature that (1) displays an appropriate copyright notice, and (2)
106 tells the user that there is no warranty for the work (except to the
107 extent that warranties are provided), that licensees may convey the
108 work under this License, and how to view a copy of this License. If
109 the interface presents a list of user commands or options, such as a
110 menu, a prominent item in the list meets this criterion.
111
112 1. Source Code.
113
114 The "source code" for a work means the preferred form of the work
115 for making modifications to it. "Object code" means any non-source
116 form of a work.
117
118 A "Standard Interface" means an interface that either is an official
119 standard defined by a recognized standards body, or, in the case of
120 interfaces specified for a particular programming language, one that
121 is widely used among developers working in that language.
122
123 The "System Libraries" of an executable work include anything, other
124 than the work as a whole, that (a) is included in the normal form of
125 packaging a Major Component, but which is not part of that Major
126 Component, and (b) serves only to enable use of the work with that
127 Major Component, or to implement a Standard Interface for which an
128 implementation is available to the public in source code form. A
129 "Major Component", in this context, means a major essential component
130 (kernel, window system, and so on) of the specific operating system
131 (if any) on which the executable work runs, or a compiler used to
132 produce the work, or an object code interpreter used to run it.
133
134 The "Corresponding Source" for a work in object code form means all
135 the source code needed to generate, install, and (for an executable
136 work) run the object code and to modify the work, including scripts to
137 control those activities. However, it does not include the work's
138 System Libraries, or general-purpose tools or generally available free
139 programs which are used unmodified in performing those activities but
140 which are not part of the work. For example, Corresponding Source
141 includes interface definition files associated with source files for
142 the work, and the source code for shared libraries and dynamically
143 linked subprograms that the work is specifically designed to require,
144 such as by intimate data communication or control flow between those
145 subprograms and other parts of the work.
146
147 The Corresponding Source need not include anything that users
148 can regenerate automatically from other parts of the Corresponding
149 Source.
150
151 The Corresponding Source for a work in source code form is that
152 same work.
153
154 2. Basic Permissions.
155
156 All rights granted under this License are granted for the term of
157 copyright on the Program, and are irrevocable provided the stated
158 conditions are met. This License explicitly affirms your unlimited
159 permission to run the unmodified Program. The output from running a
160 covered work is covered by this License only if the output, given its
161 content, constitutes a covered work. This License acknowledges your
162 rights of fair use or other equivalent, as provided by copyright law.
163
164 You may make, run and propagate covered works that you do not
165 convey, without conditions so long as your license otherwise remains
166 in force. You may convey covered works to others for the sole purpose
167 of having them make modifications exclusively for you, or provide you
168 with facilities for running those works, provided that you comply with
169 the terms of this License in conveying all material for which you do
170 not control copyright. Those thus making or running the covered works
171 for you must do so exclusively on your behalf, under your direction
172 and control, on terms that prohibit them from making any copies of
173 your copyrighted material outside their relationship with you.
174
175 Conveying under any other circumstances is permitted solely under
176 the conditions stated below. Sublicensing is not allowed; section 10
177 makes it unnecessary.
178
179 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
181 No covered work shall be deemed part of an effective technological
182 measure under any applicable law fulfilling obligations under article
183 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 similar laws prohibiting or restricting circumvention of such
185 measures.
186
187 When you convey a covered work, you waive any legal power to forbid
188 circumvention of technological measures to the extent such circumvention
189 is effected by exercising rights under this License with respect to
190 the covered work, and you disclaim any intention to limit operation or
191 modification of the work as a means of enforcing, against the work's
192 users, your or third parties' legal rights to forbid circumvention of
193 technological measures.
194
195 4. Conveying Verbatim Copies.
196
197 You may convey verbatim copies of the Program's source code as you
198 receive it, in any medium, provided that you conspicuously and
199 appropriately publish on each copy an appropriate copyright notice;
200 keep intact all notices stating that this License and any
201 non-permissive terms added in accord with section 7 apply to the code;
202 keep intact all notices of the absence of any warranty; and give all
203 recipients a copy of this License along with the Program.
204
205 You may charge any price or no price for each copy that you convey,
206 and you may offer support or warranty protection for a fee.
207
208 5. Conveying Modified Source Versions.
209
210 You may convey a work based on the Program, or the modifications to
211 produce it from the Program, in the form of source code under the
212 terms of section 4, provided that you also meet all of these conditions:
213
214 a) The work must carry prominent notices stating that you modified
215 it, and giving a relevant date.
216
217 b) The work must carry prominent notices stating that it is
218 released under this License and any conditions added under section
219 7. This requirement modifies the requirement in section 4 to
220 "keep intact all notices".
221
222 c) You must license the entire work, as a whole, under this
223 License to anyone who comes into possession of a copy. This
224 License will therefore apply, along with any applicable section 7
225 additional terms, to the whole of the work, and all its parts,
226 regardless of how they are packaged. This License gives no
227 permission to license the work in any other way, but it does not
228 invalidate such permission if you have separately received it.
229
230 d) If the work has interactive user interfaces, each must display
231 Appropriate Legal Notices; however, if the Program has interactive
232 interfaces that do not display Appropriate Legal Notices, your
233 work need not make them do so.
234
235 A compilation of a covered work with other separate and independent
236 works, which are not by their nature extensions of the covered work,
237 and which are not combined with it such as to form a larger program,
238 in or on a volume of a storage or distribution medium, is called an
239 "aggregate" if the compilation and its resulting copyright are not
240 used to limit the access or legal rights of the compilation's users
241 beyond what the individual works permit. Inclusion of a covered work
242 in an aggregate does not cause this License to apply to the other
243 parts of the aggregate.
244
245 6. Conveying Non-Source Forms.
246
247 You may convey a covered work in object code form under the terms
248 of sections 4 and 5, provided that you also convey the
249 machine-readable Corresponding Source under the terms of this License,
250 in one of these ways:
251
252 a) Convey the object code in, or embodied in, a physical product
253 (including a physical distribution medium), accompanied by the
254 Corresponding Source fixed on a durable physical medium
255 customarily used for software interchange.
256
257 b) Convey the object code in, or embodied in, a physical product
258 (including a physical distribution medium), accompanied by a
259 written offer, valid for at least three years and valid for as
260 long as you offer spare parts or customer support for that product
261 model, to give anyone who possesses the object code either (1) a
262 copy of the Corresponding Source for all the software in the
263 product that is covered by this License, on a durable physical
264 medium customarily used for software interchange, for a price no
265 more than your reasonable cost of physically performing this
266 conveying of source, or (2) access to copy the
267 Corresponding Source from a network server at no charge.
268
269 c) Convey individual copies of the object code with a copy of the
270 written offer to provide the Corresponding Source. This
271 alternative is allowed only occasionally and noncommercially, and
272 only if you received the object code with such an offer, in accord
273 with subsection 6b.
274
275 d) Convey the object code by offering access from a designated
276 place (gratis or for a charge), and offer equivalent access to the
277 Corresponding Source in the same way through the same place at no
278 further charge. You need not require recipients to copy the
279 Corresponding Source along with the object code. If the place to
280 copy the object code is a network server, the Corresponding Source
281 may be on a different server (operated by you or a third party)
282 that supports equivalent copying facilities, provided you maintain
283 clear directions next to the object code saying where to find the
284 Corresponding Source. Regardless of what server hosts the
285 Corresponding Source, you remain obligated to ensure that it is
286 available for as long as needed to satisfy these requirements.
287
288 e) Convey the object code using peer-to-peer transmission, provided
289 you inform other peers where the object code and Corresponding
290 Source of the work are being offered to the general public at no
291 charge under subsection 6d.
292
293 A separable portion of the object code, whose source code is excluded
294 from the Corresponding Source as a System Library, need not be
295 included in conveying the object code work.
296
297 A "User Product" is either (1) a "consumer product", which means any
298 tangible personal property which is normally used for personal, family,
299 or household purposes, or (2) anything designed or sold for incorporation
300 into a dwelling. In determining whether a product is a consumer product,
301 doubtful cases shall be resolved in favor of coverage. For a particular
302 product received by a particular user, "normally used" refers to a
303 typical or common use of that class of product, regardless of the status
304 of the particular user or of the way in which the particular user
305 actually uses, or expects or is expected to use, the product. A product
306 is a consumer product regardless of whether the product has substantial
307 commercial, industrial or non-consumer uses, unless such uses represent
308 the only significant mode of use of the product.
309
310 "Installation Information" for a User Product means any methods,
311 procedures, authorization keys, or other information required to install
312 and execute modified versions of a covered work in that User Product from
313 a modified version of its Corresponding Source. The information must
314 suffice to ensure that the continued functioning of the modified object
315 code is in no case prevented or interfered with solely because
316 modification has been made.
317
318 If you convey an object code work under this section in, or with, or
319 specifically for use in, a User Product, and the conveying occurs as
320 part of a transaction in which the right of possession and use of the
321 User Product is transferred to the recipient in perpetuity or for a
322 fixed term (regardless of how the transaction is characterized), the
323 Corresponding Source conveyed under this section must be accompanied
324 by the Installation Information. But this requirement does not apply
325 if neither you nor any third party retains the ability to install
326 modified object code on the User Product (for example, the work has
327 been installed in ROM).
328
329 The requirement to provide Installation Information does not include a
330 requirement to continue to provide support service, warranty, or updates
331 for a work that has been modified or installed by the recipient, or for
332 the User Product in which it has been modified or installed. Access to a
333 network may be denied when the modification itself materially and
334 adversely affects the operation of the network or violates the rules and
335 protocols for communication across the network.
336
337 Corresponding Source conveyed, and Installation Information provided,
338 in accord with this section must be in a format that is publicly
339 documented (and with an implementation available to the public in
340 source code form), and must require no special password or key for
341 unpacking, reading or copying.
342
343 7. Additional Terms.
344
345 "Additional permissions" are terms that supplement the terms of this
346 License by making exceptions from one or more of its conditions.
347 Additional permissions that are applicable to the entire Program shall
348 be treated as though they were included in this License, to the extent
349 that they are valid under applicable law. If additional permissions
350 apply only to part of the Program, that part may be used separately
351 under those permissions, but the entire Program remains governed by
352 this License without regard to the additional permissions.
353
354 When you convey a copy of a covered work, you may at your option
355 remove any additional permissions from that copy, or from any part of
356 it. (Additional permissions may be written to require their own
357 removal in certain cases when you modify the work.) You may place
358 additional permissions on material, added by you to a covered work,
359 for which you have or can give appropriate copyright permission.
360
361 Notwithstanding any other provision of this License, for material you
362 add to a covered work, you may (if authorized by the copyright holders of
363 that material) supplement the terms of this License with terms:
364
365 a) Disclaiming warranty or limiting liability differently from the
366 terms of sections 15 and 16 of this License; or
367
368 b) Requiring preservation of specified reasonable legal notices or
369 author attributions in that material or in the Appropriate Legal
370 Notices displayed by works containing it; or
371
372 c) Prohibiting misrepresentation of the origin of that material, or
373 requiring that modified versions of such material be marked in
374 reasonable ways as different from the original version; or
375
376 d) Limiting the use for publicity purposes of names of licensors or
377 authors of the material; or
378
379 e) Declining to grant rights under trademark law for use of some
380 trade names, trademarks, or service marks; or
381
382 f) Requiring indemnification of licensors and authors of that
383 material by anyone who conveys the material (or modified versions of
384 it) with contractual assumptions of liability to the recipient, for
385 any liability that these contractual assumptions directly impose on
386 those licensors and authors.
387
388 All other non-permissive additional terms are considered "further
389 restrictions" within the meaning of section 10. If the Program as you
390 received it, or any part of it, contains a notice stating that it is
391 governed by this License along with a term that is a further
392 restriction, you may remove that term. If a license document contains
393 a further restriction but permits relicensing or conveying under this
394 License, you may add to a covered work material governed by the terms
395 of that license document, provided that the further restriction does
396 not survive such relicensing or conveying.
397
398 If you add terms to a covered work in accord with this section, you
399 must place, in the relevant source files, a statement of the
400 additional terms that apply to those files, or a notice indicating
401 where to find the applicable terms.
402
403 Additional terms, permissive or non-permissive, may be stated in the
404 form of a separately written license, or stated as exceptions;
405 the above requirements apply either way.
406
407 8. Termination.
408
409 You may not propagate or modify a covered work except as expressly
410 provided under this License. Any attempt otherwise to propagate or
411 modify it is void, and will automatically terminate your rights under
412 this License (including any patent licenses granted under the third
413 paragraph of section 11).
414
415 However, if you cease all violation of this License, then your
416 license from a particular copyright holder is reinstated (a)
417 provisionally, unless and until the copyright holder explicitly and
418 finally terminates your license, and (b) permanently, if the copyright
419 holder fails to notify you of the violation by some reasonable means
420 prior to 60 days after the cessation.
421
422 Moreover, your license from a particular copyright holder is
423 reinstated permanently if the copyright holder notifies you of the
424 violation by some reasonable means, this is the first time you have
425 received notice of violation of this License (for any work) from that
426 copyright holder, and you cure the violation prior to 30 days after
427 your receipt of the notice.
428
429 Termination of your rights under this section does not terminate the
430 licenses of parties who have received copies or rights from you under
431 this License. If your rights have been terminated and not permanently
432 reinstated, you do not qualify to receive new licenses for the same
433 material under section 10.
434
435 9. Acceptance Not Required for Having Copies.
436
437 You are not required to accept this License in order to receive or
438 run a copy of the Program. Ancillary propagation of a covered work
439 occurring solely as a consequence of using peer-to-peer transmission
440 to receive a copy likewise does not require acceptance. However,
441 nothing other than this License grants you permission to propagate or
442 modify any covered work. These actions infringe copyright if you do
443 not accept this License. Therefore, by modifying or propagating a
444 covered work, you indicate your acceptance of this License to do so.
445
446 10. Automatic Licensing of Downstream Recipients.
447
448 Each time you convey a covered work, the recipient automatically
449 receives a license from the original licensors, to run, modify and
450 propagate that work, subject to this License. You are not responsible
451 for enforcing compliance by third parties with this License.
452
453 An "entity transaction" is a transaction transferring control of an
454 organization, or substantially all assets of one, or subdividing an
455 organization, or merging organizations. If propagation of a covered
456 work results from an entity transaction, each party to that
457 transaction who receives a copy of the work also receives whatever
458 licenses to the work the party's predecessor in interest had or could
459 give under the previous paragraph, plus a right to possession of the
460 Corresponding Source of the work from the predecessor in interest, if
461 the predecessor has it or can get it with reasonable efforts.
462
463 You may not impose any further restrictions on the exercise of the
464 rights granted or affirmed under this License. For example, you may
465 not impose a license fee, royalty, or other charge for exercise of
466 rights granted under this License, and you may not initiate litigation
467 (including a cross-claim or counterclaim in a lawsuit) alleging that
468 any patent claim is infringed by making, using, selling, offering for
469 sale, or importing the Program or any portion of it.
470
471 11. Patents.
472
473 A "contributor" is a copyright holder who authorizes use under this
474 License of the Program or a work on which the Program is based. The
475 work thus licensed is called the contributor's "contributor version".
476
477 A contributor's "essential patent claims" are all patent claims
478 owned or controlled by the contributor, whether already acquired or
479 hereafter acquired, that would be infringed by some manner, permitted
480 by this License, of making, using, or selling its contributor version,
481 but do not include claims that would be infringed only as a
482 consequence of further modification of the contributor version. For
483 purposes of this definition, "control" includes the right to grant
484 patent sublicenses in a manner consistent with the requirements of
485 this License.
486
487 Each contributor grants you a non-exclusive, worldwide, royalty-free
488 patent license under the contributor's essential patent claims, to
489 make, use, sell, offer for sale, import and otherwise run, modify and
490 propagate the contents of its contributor version.
491
492 In the following three paragraphs, a "patent license" is any express
493 agreement or commitment, however denominated, not to enforce a patent
494 (such as an express permission to practice a patent or covenant not to
495 sue for patent infringement). To "grant" such a patent license to a
496 party means to make such an agreement or commitment not to enforce a
497 patent against the party.
498
499 If you convey a covered work, knowingly relying on a patent license,
500 and the Corresponding Source of the work is not available for anyone
501 to copy, free of charge and under the terms of this License, through a
502 publicly available network server or other readily accessible means,
503 then you must either (1) cause the Corresponding Source to be so
504 available, or (2) arrange to deprive yourself of the benefit of the
505 patent license for this particular work, or (3) arrange, in a manner
506 consistent with the requirements of this License, to extend the patent
507 license to downstream recipients. "Knowingly relying" means you have
508 actual knowledge that, but for the patent license, your conveying the
509 covered work in a country, or your recipient's use of the covered work
510 in a country, would infringe one or more identifiable patents in that
511 country that you have reason to believe are valid.
512
513 If, pursuant to or in connection with a single transaction or
514 arrangement, you convey, or propagate by procuring conveyance of, a
515 covered work, and grant a patent license to some of the parties
516 receiving the covered work authorizing them to use, propagate, modify
517 or convey a specific copy of the covered work, then the patent license
518 you grant is automatically extended to all recipients of the covered
519 work and works based on it.
520
521 A patent license is "discriminatory" if it does not include within
522 the scope of its coverage, prohibits the exercise of, or is
523 conditioned on the non-exercise of one or more of the rights that are
524 specifically granted under this License. You may not convey a covered
525 work if you are a party to an arrangement with a third party that is
526 in the business of distributing software, under which you make payment
527 to the third party based on the extent of your activity of conveying
528 the work, and under which the third party grants, to any of the
529 parties who would receive the covered work from you, a discriminatory
530 patent license (a) in connection with copies of the covered work
531 conveyed by you (or copies made from those copies), or (b) primarily
532 for and in connection with specific products or compilations that
533 contain the covered work, unless you entered into that arrangement,
534 or that patent license was granted, prior to 28 March 2007.
535
536 Nothing in this License shall be construed as excluding or limiting
537 any implied license or other defenses to infringement that may
538 otherwise be available to you under applicable patent law.
539
540 12. No Surrender of Others' Freedom.
541
542 If conditions are imposed on you (whether by court order, agreement or
543 otherwise) that contradict the conditions of this License, they do not
544 excuse you from the conditions of this License. If you cannot convey a
545 covered work so as to satisfy simultaneously your obligations under this
546 License and any other pertinent obligations, then as a consequence you may
547 not convey it at all. For example, if you agree to terms that obligate you
548 to collect a royalty for further conveying from those to whom you convey
549 the Program, the only way you could satisfy both those terms and this
550 License would be to refrain entirely from conveying the Program.
551
552 13. Use with the GNU Affero General Public License.
553
554 Notwithstanding any other provision of this License, you have
555 permission to link or combine any covered work with a work licensed
556 under version 3 of the GNU Affero General Public License into a single
557 combined work, and to convey the resulting work. The terms of this
558 License will continue to apply to the part which is the covered work,
559 but the special requirements of the GNU Affero General Public License,
560 section 13, concerning interaction through a network will apply to the
561 combination as such.
562
563 14. Revised Versions of this License.
564
565 The Free Software Foundation may publish revised and/or new versions of
566 the GNU General Public License from time to time. Such new versions will
567 be similar in spirit to the present version, but may differ in detail to
568 address new problems or concerns.
569
570 Each version is given a distinguishing version number. If the
571 Program specifies that a certain numbered version of the GNU General
572 Public License "or any later version" applies to it, you have the
573 option of following the terms and conditions either of that numbered
574 version or of any later version published by the Free Software
575 Foundation. If the Program does not specify a version number of the
576 GNU General Public License, you may choose any version ever published
577 by the Free Software Foundation.
578
579 If the Program specifies that a proxy can decide which future
580 versions of the GNU General Public License can be used, that proxy's
581 public statement of acceptance of a version permanently authorizes you
582 to choose that version for the Program.
583
584 Later license versions may give you additional or different
585 permissions. However, no additional obligations are imposed on any
586 author or copyright holder as a result of your choosing to follow a
587 later version.
588
589 15. Disclaimer of Warranty.
590
591 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
600 16. Limitation of Liability.
601
602 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 SUCH DAMAGES.
611
612 17. Interpretation of Sections 15 and 16.
613
614 If the disclaimer of warranty and limitation of liability provided
615 above cannot be given local legal effect according to their terms,
616 reviewing courts shall apply local law that most closely approximates
617 an absolute waiver of all civil liability in connection with the
618 Program, unless a warranty or assumption of liability accompanies a
619 copy of the Program in return for a fee.
620
621 END OF TERMS AND CONDITIONS
622
623 How to Apply These Terms to Your New Programs
624
625 If you develop a new program, and you want it to be of the greatest
626 possible use to the public, the best way to achieve this is to make it
627 free software which everyone can redistribute and change under these terms.
628
629 To do so, attach the following notices to the program. It is safest
630 to attach them to the start of each source file to most effectively
631 state the exclusion of warranty; and each file should have at least
632 the "copyright" line and a pointer to where the full notice is found.
633
634 <one line to give the program's name and a brief idea of what it does.>
635 Copyright (C) <year> <name of author>
636
637 This program is free software: you can redistribute it and/or modify
638 it under the terms of the GNU General Public License as published by
639 the Free Software Foundation, either version 3 of the License, or
640 (at your option) any later version.
641
642 This program is distributed in the hope that it will be useful,
643 but WITHOUT ANY WARRANTY; without even the implied warranty of
644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 GNU General Public License for more details.
646
647 You should have received a copy of the GNU General Public License
648 along with this program. If not, see <http://www.gnu.org/licenses/>.
649
650 Also add information on how to contact you by electronic and paper mail.
651
652 If the program does terminal interaction, make it output a short
653 notice like this when it starts in an interactive mode:
654
655 <program> Copyright (C) <year> <name of author>
656 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 This is free software, and you are welcome to redistribute it
658 under certain conditions; type `show c' for details.
659
660 The hypothetical commands `show w' and `show c' should show the appropriate
661 parts of the General Public License. Of course, your program's commands
662 might be different; for a GUI interface, you would use an "about box".
663
664 You should also get your employer (if you work as a programmer) or school,
665 if any, to sign a "copyright disclaimer" for the program, if necessary.
666 For more information on this, and how to apply and follow the GNU GPL, see
667 <http://www.gnu.org/licenses/>.
668
669 The GNU General Public License does not permit incorporating your program
670 into proprietary programs. If your program is a subroutine library, you
671 may consider it more useful to permit linking proprietary applications with
672 the library. If this is what you want to do, use the GNU Lesser General
673 Public License instead of this License. But first, please read
674 <http://www.gnu.org/philosophy/why-not-lgpl.html>.
@@ -0,0 +1,13 b''
1
2
3 ===========
4 vcsserver
5 ===========
6
7 Contains the package `vcsserver`.
8
9 It provides a server to allow remote access to various version control backend
10 system.
11
12 Intention is that this package can be run independent of RhodeCode Enterprise or
13 any other non-open packages.
@@ -0,0 +1,139 b''
1 # Nix environment for the community edition
2 #
3 # This shall be as lean as possible, just producing the rhodecode-vcsserver
4 # derivation. For advanced tweaks to pimp up the development environment we use
5 # "shell.nix" so that it does not have to clutter this file.
6
7 { pkgs ? (import <nixpkgs> {})
8 , pythonPackages ? "python27Packages"
9 , pythonExternalOverrides ? self: super: {}
10 , doCheck ? true
11 }:
12
13 let pkgs_ = pkgs; in
14
15 let
16 pkgs = pkgs_.overridePackages (self: super: {
17 # Override subversion derivation to
18 # - activate python bindings
19 # - set version to 1.8
20 subversion = super.subversion18.override {
21 httpSupport = true;
22 pythonBindings = true;
23 python = self.python27Packages.python;
24 };
25 });
26
27 inherit (pkgs.lib) fix extends;
28
29 basePythonPackages = with builtins; if isAttrs pythonPackages
30 then pythonPackages
31 else getAttr pythonPackages pkgs;
32
33 elem = builtins.elem;
34 basename = path: with pkgs.lib; last (splitString "/" path);
35 startsWith = prefix: full: let
36 actualPrefix = builtins.substring 0 (builtins.stringLength prefix) full;
37 in actualPrefix == prefix;
38
39 src-filter = path: type: with pkgs.lib;
40 let
41 ext = last (splitString "." path);
42 in
43 !elem (basename path) [
44 ".git" ".hg" "__pycache__" ".eggs" "node_modules"
45 "build" "data" "tmp"] &&
46 !elem ext ["egg-info" "pyc"] &&
47 !startsWith "result" path;
48
49 rhodecode-vcsserver-src = builtins.filterSource src-filter ./.;
50
51 pythonGeneratedPackages = self: basePythonPackages.override (a: {
52 inherit self;
53 })
54 // (scopedImport {
55 self = self;
56 super = basePythonPackages;
57 inherit pkgs;
58 inherit (pkgs) fetchurl fetchgit;
59 } ./pkgs/python-packages.nix);
60
61 pythonOverrides = import ./pkgs/python-packages-overrides.nix {
62 inherit
63 basePythonPackages
64 pkgs;
65 };
66
67 pythonLocalOverrides = self: super: {
68 rhodecode-vcsserver = super.rhodecode-vcsserver.override (attrs: {
69 src = rhodecode-vcsserver-src;
70 inherit doCheck;
71
72 propagatedBuildInputs = attrs.propagatedBuildInputs ++ ([
73 pkgs.git
74 pkgs.subversion
75 ]);
76
77 # TODO: johbo: Make a nicer way to expose the parts. Maybe
78 # pkgs/default.nix?
79 passthru = {
80 pythonPackages = self;
81 };
82
83 # Somewhat snappier setup of the development environment
84 # TODO: move into shell.nix
85 # TODO: think of supporting a stable path again, so that multiple shells
86 # can share it.
87 shellHook = ''
88 # Set locale
89 export LC_ALL="en_US.UTF-8"
90
91 tmp_path=$(mktemp -d)
92 export PATH="$tmp_path/bin:$PATH"
93 export PYTHONPATH="$tmp_path/${self.python.sitePackages}:$PYTHONPATH"
94 mkdir -p $tmp_path/${self.python.sitePackages}
95 python setup.py develop --prefix $tmp_path --allow-hosts ""
96 '';
97
98 # Add VCSServer bin directory to path so that tests can find 'vcsserver'.
99 preCheck = ''
100 export PATH="$out/bin:$PATH"
101 '';
102
103 postInstall = ''
104 echo "Writing meta information for rccontrol to nix-support/rccontrol"
105 mkdir -p $out/nix-support/rccontrol
106 cp -v vcsserver/VERSION $out/nix-support/rccontrol/version
107 echo "DONE: Meta information for rccontrol written"
108
109 ln -s ${self.pyramid}/bin/* $out/bin #*/
110 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
111
112 # Symlink version control utilities
113 #
114 # We ensure that always the correct version is available as a symlink.
115 # So that users calling them via the profile path will always use the
116 # correct version.
117 ln -s ${pkgs.git}/bin/git $out/bin
118 ln -s ${self.mercurial}/bin/hg $out/bin
119 ln -s ${pkgs.subversion}/bin/svn* $out/bin
120
121 for file in $out/bin/*; do #*/
122 wrapProgram $file \
123 --prefix PYTHONPATH : $PYTHONPATH \
124 --set PYTHONHASHSEED random
125 done
126 '';
127
128 });
129 };
130
131 # Apply all overrides and fix the final package set
132 myPythonPackages =
133 (fix
134 (extends pythonExternalOverrides
135 (extends pythonLocalOverrides
136 (extends pythonOverrides
137 pythonGeneratedPackages))));
138
139 in myPythonPackages.rhodecode-vcsserver
@@ -0,0 +1,93 b''
1 ################################################################################
2 # RhodeCode VCSServer - configuration #
3 # #
4 ################################################################################
5
6 [DEFAULT]
7 host = 127.0.0.1
8 port = 9900
9 locale = en_US.UTF-8
10 # number of worker threads, this should be set based on a formula threadpool=N*6
11 # where N is number of RhodeCode Enterprise workers, eg. running 2 instances
12 # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96
13 threadpool_size = 96
14 timeout = 0
15
16 # cache regions, please don't change
17 beaker.cache.regions = repo_object
18 beaker.cache.repo_object.type = memorylru
19 beaker.cache.repo_object.max_items = 100
20 # cache auto-expires after N seconds
21 beaker.cache.repo_object.expire = 300
22 beaker.cache.repo_object.enabled = true
23
24
25 ################################
26 ### LOGGING CONFIGURATION ####
27 ################################
28 [loggers]
29 keys = root, vcsserver, pyro4, beaker
30
31 [handlers]
32 keys = console
33
34 [formatters]
35 keys = generic
36
37 #############
38 ## LOGGERS ##
39 #############
40 [logger_root]
41 level = NOTSET
42 handlers = console
43
44 [logger_vcsserver]
45 level = DEBUG
46 handlers =
47 qualname = vcsserver
48 propagate = 1
49
50 [logger_beaker]
51 level = DEBUG
52 handlers =
53 qualname = beaker
54 propagate = 1
55
56 [logger_pyro4]
57 level = DEBUG
58 handlers =
59 qualname = Pyro4
60 propagate = 1
61
62
63 ##############
64 ## HANDLERS ##
65 ##############
66
67 [handler_console]
68 class = StreamHandler
69 args = (sys.stderr,)
70 level = DEBUG
71 formatter = generic
72
73 [handler_file]
74 class = FileHandler
75 args = ('vcsserver.log', 'a',)
76 level = DEBUG
77 formatter = generic
78
79 [handler_file_rotating]
80 class = logging.handlers.TimedRotatingFileHandler
81 # 'D', 5 - rotate every 5days
82 # you can set 'h', 'midnight'
83 args = ('vcsserver.log', 'D', 5, 10,)
84 level = DEBUG
85 formatter = generic
86
87 ################
88 ## FORMATTERS ##
89 ################
90
91 [formatter_generic]
92 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
93 datefmt = %Y-%m-%d %H:%M:%S
@@ -0,0 +1,70 b''
1 [app:main]
2 use = egg:rhodecode-vcsserver
3 pyramid.reload_templates = true
4 pyramid.default_locale_name = en
5 pyramid.includes =
6 # cache regions, please don't change
7 beaker.cache.regions = repo_object
8 beaker.cache.repo_object.type = memorylru
9 beaker.cache.repo_object.max_items = 100
10 # cache auto-expires after N seconds
11 beaker.cache.repo_object.expire = 300
12 beaker.cache.repo_object.enabled = true
13 locale = en_US.UTF-8
14
15
16 [server:main]
17 use = egg:waitress#main
18 host = 0.0.0.0
19 port = %(http_port)s
20
21
22 ################################
23 ### LOGGING CONFIGURATION ####
24 ################################
25 [loggers]
26 keys = root, vcsserver, beaker
27
28 [handlers]
29 keys = console
30
31 [formatters]
32 keys = generic
33
34 #############
35 ## LOGGERS ##
36 #############
37 [logger_root]
38 level = NOTSET
39 handlers = console
40
41 [logger_vcsserver]
42 level = DEBUG
43 handlers =
44 qualname = vcsserver
45 propagate = 1
46
47 [logger_beaker]
48 level = DEBUG
49 handlers =
50 qualname = beaker
51 propagate = 1
52
53
54 ##############
55 ## HANDLERS ##
56 ##############
57
58 [handler_console]
59 class = StreamHandler
60 args = (sys.stderr,)
61 level = DEBUG
62 formatter = generic
63
64 ################
65 ## FORMATTERS ##
66 ################
67
68 [formatter_generic]
69 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
70 datefmt = %Y-%m-%d %H:%M:%S
@@ -0,0 +1,3 b''
1 [pip2nix]
2 requirements = ., -r ./requirements.txt
3 output = ./pkgs/python-packages.nix
@@ -0,0 +1,56 b''
1 # Overrides for the generated python-packages.nix
2 #
3 # This function is intended to be used as an extension to the generated file
4 # python-packages.nix. The main objective is to add needed dependencies of C
5 # libraries and tweak the build instructions where needed.
6
7 { pkgs, basePythonPackages }:
8
9 let
10 sed = "sed -i";
11 in
12
13 self: super: {
14
15 subvertpy = super.subvertpy.override (attrs: {
16 SVN_PREFIX = "${pkgs.subversion}";
17 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
18 pkgs.aprutil
19 pkgs.subversion
20 ];
21 preBuild = pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
22 ${sed} -e "s/'gcc'/'clang'/" setup.py
23 '';
24 });
25
26 mercurial = super.mercurial.override (attrs: {
27 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
28 self.python.modules.curses
29 ] ++ pkgs.lib.optional pkgs.stdenv.isDarwin
30 pkgs.darwin.apple_sdk.frameworks.ApplicationServices;
31 });
32
33 pyramid = super.pyramid.override (attrs: {
34 postFixup = ''
35 wrapPythonPrograms
36 # TODO: johbo: "wrapPython" adds this magic line which
37 # confuses pserve.
38 ${sed} '/import sys; sys.argv/d' $out/bin/.pserve-wrapped
39 '';
40 });
41
42 Pyro4 = super.Pyro4.override (attrs: {
43 # TODO: Was not able to generate this version, needs further
44 # investigation.
45 name = "Pyro4-4.35";
46 src = pkgs.fetchurl {
47 url = "https://pypi.python.org/packages/source/P/Pyro4/Pyro4-4.35.src.tar.gz";
48 md5 = "cbe6cb855f086a0f092ca075005855f3";
49 };
50 });
51
52 # Avoid that setuptools is replaced, this leads to trouble
53 # with buildPythonPackage.
54 setuptools = basePythonPackages.setuptools;
55
56 }
@@ -0,0 +1,363 b''
1 {
2 Beaker = super.buildPythonPackage {
3 name = "Beaker-1.7.0";
4 buildInputs = with self; [];
5 doCheck = false;
6 propagatedBuildInputs = with self; [];
7 src = fetchurl {
8 url = "https://pypi.python.org/packages/97/8e/409d2e7c009b8aa803dc9e6f239f1db7c3cdf578249087a404e7c27a505d/Beaker-1.7.0.tar.gz";
9 md5 = "386be3f7fe427358881eee4622b428b3";
10 };
11 };
12 Jinja2 = super.buildPythonPackage {
13 name = "Jinja2-2.8";
14 buildInputs = with self; [];
15 doCheck = false;
16 propagatedBuildInputs = with self; [MarkupSafe];
17 src = fetchurl {
18 url = "https://pypi.python.org/packages/f2/2f/0b98b06a345a761bec91a079ccae392d282690c2d8272e708f4d10829e22/Jinja2-2.8.tar.gz";
19 md5 = "edb51693fe22c53cee5403775c71a99e";
20 };
21 };
22 Mako = super.buildPythonPackage {
23 name = "Mako-1.0.4";
24 buildInputs = with self; [];
25 doCheck = false;
26 propagatedBuildInputs = with self; [MarkupSafe];
27 src = fetchurl {
28 url = "https://pypi.python.org/packages/7a/ae/925434246ee90b42e8ef57d3b30a0ab7caf9a2de3e449b876c56dcb48155/Mako-1.0.4.tar.gz";
29 md5 = "c5fc31a323dd4990683d2f2da02d4e20";
30 };
31 };
32 MarkupSafe = super.buildPythonPackage {
33 name = "MarkupSafe-0.23";
34 buildInputs = with self; [];
35 doCheck = false;
36 propagatedBuildInputs = with self; [];
37 src = fetchurl {
38 url = "https://pypi.python.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz";
39 md5 = "f5ab3deee4c37cd6a922fb81e730da6e";
40 };
41 };
42 PasteDeploy = super.buildPythonPackage {
43 name = "PasteDeploy-1.5.2";
44 buildInputs = with self; [];
45 doCheck = false;
46 propagatedBuildInputs = with self; [];
47 src = fetchurl {
48 url = "https://pypi.python.org/packages/0f/90/8e20cdae206c543ea10793cbf4136eb9a8b3f417e04e40a29d72d9922cbd/PasteDeploy-1.5.2.tar.gz";
49 md5 = "352b7205c78c8de4987578d19431af3b";
50 };
51 };
52 Pyro4 = super.buildPythonPackage {
53 name = "Pyro4-4.41";
54 buildInputs = with self; [];
55 doCheck = false;
56 propagatedBuildInputs = with self; [serpent];
57 src = fetchurl {
58 url = "https://pypi.python.org/packages/56/2b/89b566b4bf3e7f8ba790db2d1223852f8cb454c52cab7693dd41f608ca2a/Pyro4-4.41.tar.gz";
59 md5 = "ed69e9bfafa9c06c049a87cb0c4c2b6c";
60 };
61 };
62 WebOb = super.buildPythonPackage {
63 name = "WebOb-1.3.1";
64 buildInputs = with self; [];
65 doCheck = false;
66 propagatedBuildInputs = with self; [];
67 src = fetchurl {
68 url = "https://pypi.python.org/packages/16/78/adfc0380b8a0d75b2d543fa7085ba98a573b1ae486d9def88d172b81b9fa/WebOb-1.3.1.tar.gz";
69 md5 = "20918251c5726956ba8fef22d1556177";
70 };
71 };
72 WebTest = super.buildPythonPackage {
73 name = "WebTest-1.4.3";
74 buildInputs = with self; [];
75 doCheck = false;
76 propagatedBuildInputs = with self; [WebOb];
77 src = fetchurl {
78 url = "https://pypi.python.org/packages/51/3d/84fd0f628df10b30c7db87895f56d0158e5411206b721ca903cb51bfd948/WebTest-1.4.3.zip";
79 md5 = "631ce728bed92c681a4020a36adbc353";
80 };
81 };
82 configobj = super.buildPythonPackage {
83 name = "configobj-5.0.6";
84 buildInputs = with self; [];
85 doCheck = false;
86 propagatedBuildInputs = with self; [six];
87 src = fetchurl {
88 url = "https://pypi.python.org/packages/64/61/079eb60459c44929e684fa7d9e2fdca403f67d64dd9dbac27296be2e0fab/configobj-5.0.6.tar.gz";
89 md5 = "e472a3a1c2a67bb0ec9b5d54c13a47d6";
90 };
91 };
92 dulwich = super.buildPythonPackage {
93 name = "dulwich-0.12.0";
94 buildInputs = with self; [];
95 doCheck = false;
96 propagatedBuildInputs = with self; [];
97 src = fetchurl {
98 url = "https://pypi.python.org/packages/6f/04/fbe561b6d45c0ec758330d5b7f5ba4b6cb4f1ca1ab49859d2fc16320da75/dulwich-0.12.0.tar.gz";
99 md5 = "f3a8a12bd9f9dd8c233e18f3d49436fa";
100 };
101 };
102 greenlet = super.buildPythonPackage {
103 name = "greenlet-0.4.7";
104 buildInputs = with self; [];
105 doCheck = false;
106 propagatedBuildInputs = with self; [];
107 src = fetchurl {
108 url = "https://pypi.python.org/packages/7a/9f/a1a0d9bdf3203ae1502c5a8434fe89d323599d78a106985bc327351a69d4/greenlet-0.4.7.zip";
109 md5 = "c2333a8ff30fa75c5d5ec0e67b461086";
110 };
111 };
112 gunicorn = super.buildPythonPackage {
113 name = "gunicorn-19.3.0";
114 buildInputs = with self; [];
115 doCheck = false;
116 propagatedBuildInputs = with self; [];
117 src = fetchurl {
118 url = "https://pypi.python.org/packages/b0/3d/c476010c920926d2b5b4be0a9a5f5dc0a50c667476ad4737774d44fa7591/gunicorn-19.3.0.tar.gz";
119 md5 = "faa3e80661efd67e5e06bba32699af20";
120 };
121 };
122 hgsubversion = super.buildPythonPackage {
123 name = "hgsubversion-1.8.5";
124 buildInputs = with self; [];
125 doCheck = false;
126 propagatedBuildInputs = with self; [mercurial subvertpy];
127 src = fetchurl {
128 url = "https://pypi.python.org/packages/f7/8d/3e5719405d4b0b57db7faaf472fb836ed4c437a82bd124a2a37707c33bff/hgsubversion-1.8.5.tar.gz";
129 md5 = "afc3f096fb4dacf1d9210811f81313e0";
130 };
131 };
132 infrae.cache = super.buildPythonPackage {
133 name = "infrae.cache-1.0.1";
134 buildInputs = with self; [];
135 doCheck = false;
136 propagatedBuildInputs = with self; [Beaker repoze.lru];
137 src = fetchurl {
138 url = "https://pypi.python.org/packages/bb/f0/e7d5e984cf6592fd2807dc7bc44a93f9d18e04e6a61f87fdfb2622422d74/infrae.cache-1.0.1.tar.gz";
139 md5 = "b09076a766747e6ed2a755cc62088e32";
140 };
141 };
142 mercurial = super.buildPythonPackage {
143 name = "mercurial-3.7.3";
144 buildInputs = with self; [];
145 doCheck = false;
146 propagatedBuildInputs = with self; [];
147 src = fetchurl {
148 url = "https://pypi.python.org/packages/e8/a0/fe6bf60a314a30299c58a5ed67de9fffeae04731201f10dc2822befb062d/mercurial-3.7.3.tar.gz";
149 md5 = "f47c9c76b7bf429dafecb71fa81c01b4";
150 };
151 };
152 mock = super.buildPythonPackage {
153 name = "mock-1.0.1";
154 buildInputs = with self; [];
155 doCheck = false;
156 propagatedBuildInputs = with self; [];
157 src = fetchurl {
158 url = "https://pypi.python.org/packages/15/45/30273ee91feb60dabb8fbb2da7868520525f02cf910279b3047182feed80/mock-1.0.1.zip";
159 md5 = "869f08d003c289a97c1a6610faf5e913";
160 };
161 };
162 msgpack-python = super.buildPythonPackage {
163 name = "msgpack-python-0.4.6";
164 buildInputs = with self; [];
165 doCheck = false;
166 propagatedBuildInputs = with self; [];
167 src = fetchurl {
168 url = "https://pypi.python.org/packages/15/ce/ff2840885789ef8035f66cd506ea05bdb228340307d5e71a7b1e3f82224c/msgpack-python-0.4.6.tar.gz";
169 md5 = "8b317669314cf1bc881716cccdaccb30";
170 };
171 };
172 py = super.buildPythonPackage {
173 name = "py-1.4.29";
174 buildInputs = with self; [];
175 doCheck = false;
176 propagatedBuildInputs = with self; [];
177 src = fetchurl {
178 url = "https://pypi.python.org/packages/2a/bc/a1a4a332ac10069b8e5e25136a35e08a03f01fd6ab03d819889d79a1fd65/py-1.4.29.tar.gz";
179 md5 = "c28e0accba523a29b35a48bb703fb96c";
180 };
181 };
182 pyramid = super.buildPythonPackage {
183 name = "pyramid-1.6.1";
184 buildInputs = with self; [];
185 doCheck = false;
186 propagatedBuildInputs = with self; [setuptools WebOb repoze.lru zope.interface zope.deprecation venusian translationstring PasteDeploy];
187 src = fetchurl {
188 url = "https://pypi.python.org/packages/30/b3/fcc4a2a4800cbf21989e00454b5828cf1f7fe35c63e0810b350e56d4c475/pyramid-1.6.1.tar.gz";
189 md5 = "b18688ff3cc33efdbb098a35b45dd122";
190 };
191 };
192 pyramid-jinja2 = super.buildPythonPackage {
193 name = "pyramid-jinja2-2.5";
194 buildInputs = with self; [];
195 doCheck = false;
196 propagatedBuildInputs = with self; [pyramid zope.deprecation Jinja2 MarkupSafe];
197 src = fetchurl {
198 url = "https://pypi.python.org/packages/a1/80/595e26ffab7deba7208676b6936b7e5a721875710f982e59899013cae1ed/pyramid_jinja2-2.5.tar.gz";
199 md5 = "07cb6547204ac5e6f0b22a954ccee928";
200 };
201 };
202 pyramid-mako = super.buildPythonPackage {
203 name = "pyramid-mako-1.0.2";
204 buildInputs = with self; [];
205 doCheck = false;
206 propagatedBuildInputs = with self; [pyramid Mako];
207 src = fetchurl {
208 url = "https://pypi.python.org/packages/f1/92/7e69bcf09676d286a71cb3bbb887b16595b96f9ba7adbdc239ffdd4b1eb9/pyramid_mako-1.0.2.tar.gz";
209 md5 = "ee25343a97eb76bd90abdc2a774eb48a";
210 };
211 };
212 pytest = super.buildPythonPackage {
213 name = "pytest-2.8.5";
214 buildInputs = with self; [];
215 doCheck = false;
216 propagatedBuildInputs = with self; [py];
217 src = fetchurl {
218 url = "https://pypi.python.org/packages/b1/3d/d7ea9b0c51e0cacded856e49859f0a13452747491e842c236bbab3714afe/pytest-2.8.5.zip";
219 md5 = "8493b06f700862f1294298d6c1b715a9";
220 };
221 };
222 repoze.lru = super.buildPythonPackage {
223 name = "repoze.lru-0.6";
224 buildInputs = with self; [];
225 doCheck = false;
226 propagatedBuildInputs = with self; [];
227 src = fetchurl {
228 url = "https://pypi.python.org/packages/6e/1e/aa15cc90217e086dc8769872c8778b409812ff036bf021b15795638939e4/repoze.lru-0.6.tar.gz";
229 md5 = "2c3b64b17a8e18b405f55d46173e14dd";
230 };
231 };
232 rhodecode-vcsserver = super.buildPythonPackage {
233 name = "rhodecode-vcsserver-4.0.0";
234 buildInputs = with self; [mock pytest WebTest];
235 doCheck = true;
236 propagatedBuildInputs = with self; [configobj dulwich hgsubversion infrae.cache mercurial msgpack-python pyramid Pyro4 simplejson subprocess32 waitress WebOb];
237 src = ./.;
238 };
239 serpent = super.buildPythonPackage {
240 name = "serpent-1.12";
241 buildInputs = with self; [];
242 doCheck = false;
243 propagatedBuildInputs = with self; [];
244 src = fetchurl {
245 url = "https://pypi.python.org/packages/3b/19/1e0e83b47c09edaef8398655088036e7e67386b5c48770218ebb339fbbd5/serpent-1.12.tar.gz";
246 md5 = "05869ac7b062828b34f8f927f0457b65";
247 };
248 };
249 setuptools = super.buildPythonPackage {
250 name = "setuptools-20.8.1";
251 buildInputs = with self; [];
252 doCheck = false;
253 propagatedBuildInputs = with self; [];
254 src = fetchurl {
255 url = "https://pypi.python.org/packages/c4/19/c1bdc88b53da654df43770f941079dbab4e4788c2dcb5658fb86259894c7/setuptools-20.8.1.zip";
256 md5 = "fe58a5cac0df20bb83942b252a4b0543";
257 };
258 };
259 simplejson = super.buildPythonPackage {
260 name = "simplejson-3.7.2";
261 buildInputs = with self; [];
262 doCheck = false;
263 propagatedBuildInputs = with self; [];
264 src = fetchurl {
265 url = "https://pypi.python.org/packages/6d/89/7f13f099344eea9d6722779a1f165087cb559598107844b1ac5dbd831fb1/simplejson-3.7.2.tar.gz";
266 md5 = "a5fc7d05d4cb38492285553def5d4b46";
267 };
268 };
269 six = super.buildPythonPackage {
270 name = "six-1.9.0";
271 buildInputs = with self; [];
272 doCheck = false;
273 propagatedBuildInputs = with self; [];
274 src = fetchurl {
275 url = "https://pypi.python.org/packages/16/64/1dc5e5976b17466fd7d712e59cbe9fb1e18bec153109e5ba3ed6c9102f1a/six-1.9.0.tar.gz";
276 md5 = "476881ef4012262dfc8adc645ee786c4";
277 };
278 };
279 subprocess32 = super.buildPythonPackage {
280 name = "subprocess32-3.2.6";
281 buildInputs = with self; [];
282 doCheck = false;
283 propagatedBuildInputs = with self; [];
284 src = fetchurl {
285 url = "https://pypi.python.org/packages/28/8d/33ccbff51053f59ae6c357310cac0e79246bbed1d345ecc6188b176d72c3/subprocess32-3.2.6.tar.gz";
286 md5 = "754c5ab9f533e764f931136974b618f1";
287 };
288 };
289 subvertpy = super.buildPythonPackage {
290 name = "subvertpy-0.9.3";
291 buildInputs = with self; [];
292 doCheck = false;
293 propagatedBuildInputs = with self; [];
294 src = fetchurl {
295 url = "https://github.com/jelmer/subvertpy/archive/subvertpy-0.9.3.tar.gz";
296 md5 = "7b745a47128050ea5a73efcd913ec1cf";
297 };
298 };
299 translationstring = super.buildPythonPackage {
300 name = "translationstring-1.3";
301 buildInputs = with self; [];
302 doCheck = false;
303 propagatedBuildInputs = with self; [];
304 src = fetchurl {
305 url = "https://pypi.python.org/packages/5e/eb/bee578cc150b44c653b63f5ebe258b5d0d812ddac12497e5f80fcad5d0b4/translationstring-1.3.tar.gz";
306 md5 = "a4b62e0f3c189c783a1685b3027f7c90";
307 };
308 };
309 venusian = super.buildPythonPackage {
310 name = "venusian-1.0";
311 buildInputs = with self; [];
312 doCheck = false;
313 propagatedBuildInputs = with self; [];
314 src = fetchurl {
315 url = "https://pypi.python.org/packages/86/20/1948e0dfc4930ddde3da8c33612f6a5717c0b4bc28f591a5c5cf014dd390/venusian-1.0.tar.gz";
316 md5 = "dccf2eafb7113759d60c86faf5538756";
317 };
318 };
319 waitress = super.buildPythonPackage {
320 name = "waitress-0.8.9";
321 buildInputs = with self; [];
322 doCheck = false;
323 propagatedBuildInputs = with self; [setuptools];
324 src = fetchurl {
325 url = "https://pypi.python.org/packages/ee/65/fc9dee74a909a1187ca51e4f15ad9c4d35476e4ab5813f73421505c48053/waitress-0.8.9.tar.gz";
326 md5 = "da3f2e62b3676be5dd630703a68e2a04";
327 };
328 };
329 wheel = super.buildPythonPackage {
330 name = "wheel-0.29.0";
331 buildInputs = with self; [];
332 doCheck = false;
333 propagatedBuildInputs = with self; [];
334 src = fetchurl {
335 url = "https://pypi.python.org/packages/c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/wheel-0.29.0.tar.gz";
336 md5 = "555a67e4507cedee23a0deb9651e452f";
337 };
338 };
339 zope.deprecation = super.buildPythonPackage {
340 name = "zope.deprecation-4.1.1";
341 buildInputs = with self; [];
342 doCheck = false;
343 propagatedBuildInputs = with self; [setuptools];
344 src = fetchurl {
345 url = "https://pypi.python.org/packages/c5/c9/e760f131fcde817da6c186a3f4952b8f206b7eeb269bb6f0836c715c5f20/zope.deprecation-4.1.1.tar.gz";
346 md5 = "ce261b9384066f7e13b63525778430cb";
347 };
348 };
349 zope.interface = super.buildPythonPackage {
350 name = "zope.interface-4.1.3";
351 buildInputs = with self; [];
352 doCheck = false;
353 propagatedBuildInputs = with self; [setuptools];
354 src = fetchurl {
355 url = "https://pypi.python.org/packages/9d/81/2509ca3c6f59080123c1a8a97125eb48414022618cec0e64eb1313727bfe/zope.interface-4.1.3.tar.gz";
356 md5 = "9ae3d24c0c7415deb249dd1a132f0f79";
357 };
358 };
359
360 ### Test requirements
361
362
363 }
@@ -0,0 +1,93 b''
1 ################################################################################
2 # RhodeCode VCSServer - configuration #
3 # #
4 ################################################################################
5
6 [DEFAULT]
7 host = 127.0.0.1
8 port = 9900
9 locale = en_US.UTF-8
10 # number of worker threads, this should be set based on a formula threadpool=N*6
11 # where N is number of RhodeCode Enterprise workers, eg. running 2 instances
12 # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96
13 threadpool_size = 96
14 timeout = 0
15
16 # cache regions, please don't change
17 beaker.cache.regions = repo_object
18 beaker.cache.repo_object.type = memorylru
19 beaker.cache.repo_object.max_items = 100
20 # cache auto-expires after N seconds
21 beaker.cache.repo_object.expire = 300
22 beaker.cache.repo_object.enabled = true
23
24
25 ################################
26 ### LOGGING CONFIGURATION ####
27 ################################
28 [loggers]
29 keys = root, vcsserver, pyro4, beaker
30
31 [handlers]
32 keys = console
33
34 [formatters]
35 keys = generic
36
37 #############
38 ## LOGGERS ##
39 #############
40 [logger_root]
41 level = NOTSET
42 handlers = console
43
44 [logger_vcsserver]
45 level = DEBUG
46 handlers =
47 qualname = vcsserver
48 propagate = 1
49
50 [logger_beaker]
51 level = DEBUG
52 handlers =
53 qualname = beaker
54 propagate = 1
55
56 [logger_pyro4]
57 level = DEBUG
58 handlers =
59 qualname = Pyro4
60 propagate = 1
61
62
63 ##############
64 ## HANDLERS ##
65 ##############
66
67 [handler_console]
68 class = StreamHandler
69 args = (sys.stderr,)
70 level = DEBUG
71 formatter = generic
72
73 [handler_file]
74 class = FileHandler
75 args = ('vcsserver.log', 'a',)
76 level = DEBUG
77 formatter = generic
78
79 [handler_file_rotating]
80 class = logging.handlers.TimedRotatingFileHandler
81 # 'D', 5 - rotate every 5days
82 # you can set 'h', 'midnight'
83 args = ('vcsserver.log', 'D', 5, 10,)
84 level = DEBUG
85 formatter = generic
86
87 ################
88 ## FORMATTERS ##
89 ################
90
91 [formatter_generic]
92 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
93 datefmt = %Y-%m-%d %H:%M:%S
@@ -0,0 +1,13 b''
1 { pkgs ? import <nixpkgs> {}
2 }:
3
4 let
5
6 vcsserver = import ./default.nix {
7 inherit
8 pkgs;
9 };
10
11 in {
12 build = vcsserver;
13 }
@@ -0,0 +1,34 b''
1 Beaker==1.7.0
2 configobj==5.0.6
3 dulwich==0.12.0
4 hgsubversion==1.8.5
5 infrae.cache==1.0.1
6 mercurial==3.7.3
7 msgpack-python==0.4.6
8 py==1.4.29
9 pyramid==1.6.1
10 pyramid-jinja2==2.5
11 pyramid-mako==1.0.2
12 Pyro4==4.41
13 pytest==2.8.5
14 repoze.lru==0.6
15 serpent==1.12
16 setuptools==20.8.1
17 simplejson==3.7.2
18 subprocess32==3.2.6
19 # TODO: johbo: This version is not in source on PyPI currently,
20 # change back once this or a future version is available
21 https://github.com/jelmer/subvertpy/archive/subvertpy-0.9.3.tar.gz#md5=7b745a47128050ea5a73efcd913ec1cf
22 six==1.9.0
23 translationstring==1.3
24 waitress==0.8.9
25 WebOb==1.3.1
26 wheel==0.29.0
27 zope.deprecation==4.1.1
28 zope.interface==4.1.3
29 greenlet==0.4.7
30 gunicorn==19.3.0
31
32 # Test related requirements
33 mock==1.0.1
34 WebTest==1.4.3
@@ -0,0 +1,102 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 from setuptools import setup, find_packages
19 from setuptools.command.test import test as TestCommand
20 from codecs import open
21 from os import path
22 import pkgutil
23 import sys
24
25
26 here = path.abspath(path.dirname(__file__))
27
28 with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
29 long_description = f.read()
30
31
32 def get_version():
33 version = pkgutil.get_data('vcsserver', 'VERSION')
34 return version.strip()
35
36
37 class PyTest(TestCommand):
38 user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
39
40 def initialize_options(self):
41 TestCommand.initialize_options(self)
42 self.pytest_args = []
43
44 def finalize_options(self):
45 TestCommand.finalize_options(self)
46 self.test_args = []
47 self.test_suite = True
48
49 def run_tests(self):
50 # import here, cause outside the eggs aren't loaded
51 import pytest
52 errno = pytest.main(self.pytest_args)
53 sys.exit(errno)
54
55
56 setup(
57 name='rhodecode-vcsserver',
58 version=get_version(),
59 description='Version Control System Server',
60 long_description=long_description,
61 url='http://www.rhodecode.com',
62 author='RhodeCode GmbH',
63 author_email='marcin@rhodecode.com',
64 cmdclass={'test': PyTest},
65 license='GPLv3',
66 classifiers=[
67 'Development Status :: 5 - Production/Stable',
68 'Intended Audience :: Developers',
69 'Topic :: Software Development :: Version Control',
70 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
71 'Programming Language :: Python :: 2.7',
72 ],
73 packages=find_packages(),
74 tests_require=[
75 'mock',
76 'pytest',
77 'WebTest',
78 ],
79 install_requires=[
80 'configobj',
81 'dulwich',
82 'hgsubversion',
83 'infrae.cache',
84 'mercurial',
85 'msgpack-python',
86 'pyramid',
87 'Pyro4',
88 'simplejson',
89 'subprocess32',
90 'waitress',
91 'WebOb',
92 ],
93 package_data={
94 'vcsserver': ['VERSION'],
95 },
96 entry_points={
97 'console_scripts': [
98 'vcsserver=vcsserver.main:main',
99 ],
100 'paste.app_factory': ['main=vcsserver.http_main:main']
101 },
102 )
@@ -0,0 +1,13 b''
1 { pkgs ? (import <nixpkgs> {})
2 }:
3
4 let
5 vcsserver = import ./default.nix {inherit pkgs;};
6
7 in vcsserver.override (attrs: {
8
9 # Avoid that we dump any sources into the store when entering the shell and
10 # make development a little bit more convenient.
11 src = null;
12
13 })
@@ -0,0 +1,93 b''
1 ################################################################################
2 # RhodeCode VCSServer - configuration #
3 # #
4 ################################################################################
5
6 [DEFAULT]
7 host = 127.0.0.1
8 port = 9901
9 locale = en_US.UTF-8
10 # number of worker threads, this should be set based on a formula threadpool=N*6
11 # where N is number of RhodeCode Enterprise workers, eg. running 2 instances
12 # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96
13 threadpool_size = 96
14 timeout = 0
15
16 # cache regions, please don't change
17 beaker.cache.regions = repo_object
18 beaker.cache.repo_object.type = memorylru
19 beaker.cache.repo_object.max_items = 100
20 # cache auto-expires after N seconds
21 beaker.cache.repo_object.expire = 300
22 beaker.cache.repo_object.enabled = true
23
24
25 ################################
26 ### LOGGING CONFIGURATION ####
27 ################################
28 [loggers]
29 keys = root, vcsserver, pyro4, beaker
30
31 [handlers]
32 keys = console
33
34 [formatters]
35 keys = generic
36
37 #############
38 ## LOGGERS ##
39 #############
40 [logger_root]
41 level = NOTSET
42 handlers = console
43
44 [logger_vcsserver]
45 level = DEBUG
46 handlers =
47 qualname = vcsserver
48 propagate = 1
49
50 [logger_beaker]
51 level = DEBUG
52 handlers =
53 qualname = beaker
54 propagate = 1
55
56 [logger_pyro4]
57 level = DEBUG
58 handlers =
59 qualname = Pyro4
60 propagate = 1
61
62
63 ##############
64 ## HANDLERS ##
65 ##############
66
67 [handler_console]
68 class = StreamHandler
69 args = (sys.stderr,)
70 level = INFO
71 formatter = generic
72
73 [handler_file]
74 class = FileHandler
75 args = ('vcsserver.log', 'a',)
76 level = DEBUG
77 formatter = generic
78
79 [handler_file_rotating]
80 class = logging.handlers.TimedRotatingFileHandler
81 # 'D', 5 - rotate every 5days
82 # you can set 'h', 'midnight'
83 args = ('vcsserver.log', 'D', 5, 10,)
84 level = DEBUG
85 formatter = generic
86
87 ################
88 ## FORMATTERS ##
89 ################
90
91 [formatter_generic]
92 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
93 datefmt = %Y-%m-%d %H:%M:%S
@@ -0,0 +1,57 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import socket
19
20 import pytest
21
22
23 def pytest_addoption(parser):
24 parser.addoption(
25 '--repeat', type=int, default=100,
26 help="Number of repetitions in performance tests.")
27
28
29 @pytest.fixture(scope='session')
30 def repeat(request):
31 """
32 The number of repetitions is based on this fixture.
33
34 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
35 tests are not too slow in our default test suite.
36 """
37 return request.config.getoption('--repeat')
38
39
40 @pytest.fixture(scope='session')
41 def vcsserver_port(request):
42 port = get_available_port()
43 print 'Using vcsserver port %s' % (port, )
44 return port
45
46
47 def get_available_port():
48 family = socket.AF_INET
49 socktype = socket.SOCK_STREAM
50 host = '127.0.0.1'
51
52 mysocket = socket.socket(family, socktype)
53 mysocket.bind((host, 0))
54 port = mysocket.getsockname()[1]
55 mysocket.close()
56 del mysocket
57 return port
@@ -0,0 +1,71 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import os
19 import shutil
20 import tempfile
21
22 import configobj
23
24
25 class TestINI(object):
26 """
27 Allows to create a new test.ini file as a copy of existing one with edited
28 data. If existing file is not present, it creates a new one. Example usage::
29
30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
31 print 'vcsserver --config=%s' % new_test_ini
32 """
33
34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
35 destroy=True):
36 self.ini_file_path = ini_file_path
37 self.ini_params = ini_params
38 self.new_path = None
39 self.new_path_prefix = new_file_prefix or 'test'
40 self.destroy = destroy
41
42 def __enter__(self):
43 _, pref = tempfile.mkstemp()
44 loc = tempfile.gettempdir()
45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
46 pref, self.new_path_prefix, self.ini_file_path))
47
48 # copy ini file and modify according to the params, if we re-use a file
49 if os.path.isfile(self.ini_file_path):
50 shutil.copy(self.ini_file_path, self.new_path)
51 else:
52 # create new dump file for configObj to write to.
53 with open(self.new_path, 'wb'):
54 pass
55
56 config = configobj.ConfigObj(
57 self.new_path, file_error=True, write_empty_values=True)
58
59 for data in self.ini_params:
60 section, ini_params = data.items()[0]
61 key, val = ini_params.items()[0]
62 if section not in config:
63 config[section] = {}
64 config[section][key] = val
65
66 config.write()
67 return self.new_path
68
69 def __exit__(self, exc_type, exc_val, exc_tb):
70 if self.destroy:
71 os.remove(self.new_path)
@@ -0,0 +1,162 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import inspect
19
20 import pytest
21 import dulwich.errors
22 from mock import Mock, patch
23
24 from vcsserver import git
25
26
27 SAMPLE_REFS = {
28 'HEAD': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
29 'refs/tags/v0.1.9': '341d28f0eec5ddf0b6b77871e13c2bbd6bec685c',
30 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
31 'refs/tags/v0.1.1': 'e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0',
32 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
33 }
34
35
36 @pytest.fixture
37 def git_remote():
38 """
39 A GitRemote instance with a mock factory.
40 """
41 factory = Mock()
42 remote = git.GitRemote(factory)
43 return remote
44
45
46 def test_discover_git_version(git_remote):
47 version = git_remote.discover_git_version()
48 assert version
49
50
51 class TestGitFetch(object):
52 def setup(self):
53 self.mock_repo = Mock()
54 factory = Mock()
55 factory.repo = Mock(return_value=self.mock_repo)
56 self.remote_git = git.GitRemote(factory)
57
58 def test_fetches_all_when_no_commit_ids_specified(self):
59 def side_effect(determine_wants, *args, **kwargs):
60 determine_wants(SAMPLE_REFS)
61
62 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
63 mock_fetch.side_effect = side_effect
64 self.remote_git.fetch(wire=None, url='/tmp/', apply_refs=False)
65 determine_wants = self.mock_repo.object_store.determine_wants_all
66 determine_wants.assert_called_once_with(SAMPLE_REFS)
67
68 def test_fetches_specified_commits(self):
69 selected_refs = {
70 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
71 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
72 }
73
74 def side_effect(determine_wants, *args, **kwargs):
75 result = determine_wants(SAMPLE_REFS)
76 assert sorted(result) == sorted(selected_refs.values())
77 return result
78
79 with patch('dulwich.client.LocalGitClient.fetch') as mock_fetch:
80 mock_fetch.side_effect = side_effect
81 self.remote_git.fetch(
82 wire=None, url='/tmp/', apply_refs=False,
83 refs=selected_refs.keys())
84 determine_wants = self.mock_repo.object_store.determine_wants_all
85 assert determine_wants.call_count == 0
86
87 def test_get_remote_refs(self):
88 factory = Mock()
89 remote_git = git.GitRemote(factory)
90 url = 'http://example.com/test/test.git'
91 sample_refs = {
92 'refs/tags/v0.1.8': '74ebce002c088b8a5ecf40073db09375515ecd68',
93 'refs/tags/v0.1.3': '5a3a8fb005554692b16e21dee62bf02667d8dc3e',
94 }
95
96 with patch('vcsserver.git.Repo', create=False) as mock_repo:
97 mock_repo().get_refs.return_value = sample_refs
98 remote_refs = remote_git.get_remote_refs(wire=None, url=url)
99 mock_repo().get_refs.assert_called_once_with()
100 assert remote_refs == sample_refs
101
102 def test_remove_ref(self):
103 ref_to_remove = 'refs/tags/v0.1.9'
104 self.mock_repo.refs = SAMPLE_REFS.copy()
105 self.remote_git.remove_ref(None, ref_to_remove)
106 assert ref_to_remove not in self.mock_repo.refs
107
108
109 class TestReraiseSafeExceptions(object):
110 def test_method_decorated_with_reraise_safe_exceptions(self):
111 factory = Mock()
112 git_remote = git.GitRemote(factory)
113
114 def fake_function():
115 return None
116
117 decorator = git.reraise_safe_exceptions(fake_function)
118
119 methods = inspect.getmembers(git_remote, predicate=inspect.ismethod)
120 for method_name, method in methods:
121 if not method_name.startswith('_'):
122 assert method.im_func.__code__ == decorator.__code__
123
124 @pytest.mark.parametrize('side_effect, expected_type', [
125 (dulwich.errors.ChecksumMismatch('0000000', 'deadbeef'), 'lookup'),
126 (dulwich.errors.NotCommitError('deadbeef'), 'lookup'),
127 (dulwich.errors.MissingCommitError('deadbeef'), 'lookup'),
128 (dulwich.errors.ObjectMissing('deadbeef'), 'lookup'),
129 (dulwich.errors.HangupException(), 'error'),
130 (dulwich.errors.UnexpectedCommandError('test-cmd'), 'error'),
131 ])
132 def test_safe_exceptions_reraised(self, side_effect, expected_type):
133 @git.reraise_safe_exceptions
134 def fake_method():
135 raise side_effect
136
137 with pytest.raises(Exception) as exc_info:
138 fake_method()
139 assert type(exc_info.value) == Exception
140 assert exc_info.value._vcs_kind == expected_type
141
142
143 class TestDulwichRepoWrapper(object):
144 def test_calls_close_on_delete(self):
145 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
146 with isdir_patcher:
147 repo = git.Repo('/tmp/abcde')
148 with patch.object(git.DulwichRepo, 'close') as close_mock:
149 del repo
150 close_mock.assert_called_once_with()
151
152
153 class TestGitFactory(object):
154 def test_create_repo_returns_dulwich_wrapper(self):
155 factory = git.GitFactory(repo_cache=Mock())
156 wire = {
157 'path': '/tmp/abcde'
158 }
159 isdir_patcher = patch('dulwich.repo.os.path.isdir', return_value=True)
160 with isdir_patcher:
161 result = factory._create_repo(wire, True)
162 assert isinstance(result, git.Repo)
@@ -0,0 +1,127 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import inspect
19 import sys
20 import traceback
21
22 import pytest
23 from mercurial.error import LookupError
24 from mock import Mock, MagicMock, patch
25
26 from vcsserver import exceptions, hg, hgcompat
27
28
29 class TestHGLookup(object):
30 def setup(self):
31 self.mock_repo = MagicMock()
32 self.mock_repo.__getitem__.side_effect = LookupError(
33 'revision_or_commit_id', 'index', 'message')
34 factory = Mock()
35 factory.repo = Mock(return_value=self.mock_repo)
36 self.remote_hg = hg.HgRemote(factory)
37
38 def test_fail_lookup_hg(self):
39 with pytest.raises(Exception) as exc_info:
40 self.remote_hg.lookup(
41 wire=None, revision='revision_or_commit_id', both=True)
42
43 assert exc_info.value._vcs_kind == 'lookup'
44 assert 'revision_or_commit_id' in exc_info.value.args
45
46
47 class TestDiff(object):
48 def test_raising_safe_exception_when_lookup_failed(self):
49 repo = Mock()
50 factory = Mock()
51 factory.repo = Mock(return_value=repo)
52 hg_remote = hg.HgRemote(factory)
53 with patch('mercurial.patch.diff') as diff_mock:
54 diff_mock.side_effect = LookupError(
55 'deadbeef', 'index', 'message')
56 with pytest.raises(Exception) as exc_info:
57 hg_remote.diff(
58 wire=None, rev1='deadbeef', rev2='deadbee1',
59 file_filter=None, opt_git=True, opt_ignorews=True,
60 context=3)
61 assert type(exc_info.value) == Exception
62 assert exc_info.value._vcs_kind == 'lookup'
63
64
65 class TestReraiseSafeExceptions(object):
66 def test_method_decorated_with_reraise_safe_exceptions(self):
67 factory = Mock()
68 hg_remote = hg.HgRemote(factory)
69 methods = inspect.getmembers(hg_remote, predicate=inspect.ismethod)
70 decorator = hg.reraise_safe_exceptions(None)
71 for method_name, method in methods:
72 if not method_name.startswith('_'):
73 assert method.im_func.__code__ == decorator.__code__
74
75 @pytest.mark.parametrize('side_effect, expected_type', [
76 (hgcompat.Abort(), 'abort'),
77 (hgcompat.InterventionRequired(), 'abort'),
78 (hgcompat.RepoLookupError(), 'lookup'),
79 (hgcompat.LookupError('deadbeef', 'index', 'message'), 'lookup'),
80 (hgcompat.RepoError(), 'error'),
81 (hgcompat.RequirementError(), 'requirement'),
82 ])
83 def test_safe_exceptions_reraised(self, side_effect, expected_type):
84 @hg.reraise_safe_exceptions
85 def fake_method():
86 raise side_effect
87
88 with pytest.raises(Exception) as exc_info:
89 fake_method()
90 assert type(exc_info.value) == Exception
91 assert exc_info.value._vcs_kind == expected_type
92
93 def test_keeps_original_traceback(self):
94 @hg.reraise_safe_exceptions
95 def fake_method():
96 try:
97 raise hgcompat.Abort()
98 except:
99 self.original_traceback = traceback.format_tb(
100 sys.exc_info()[2])
101 raise
102
103 try:
104 fake_method()
105 except Exception:
106 new_traceback = traceback.format_tb(sys.exc_info()[2])
107
108 new_traceback_tail = new_traceback[-len(self.original_traceback):]
109 assert new_traceback_tail == self.original_traceback
110
111 def test_maps_unknow_exceptions_to_unhandled(self):
112 @hg.reraise_safe_exceptions
113 def stub_method():
114 raise ValueError('stub')
115
116 with pytest.raises(Exception) as exc_info:
117 stub_method()
118 assert exc_info.value._vcs_kind == 'unhandled'
119
120 def test_does_not_map_known_exceptions(self):
121 @hg.reraise_safe_exceptions
122 def stub_method():
123 raise exceptions.LookupException('stub')
124
125 with pytest.raises(Exception) as exc_info:
126 stub_method()
127 assert exc_info.value._vcs_kind == 'lookup'
@@ -0,0 +1,125 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import mock
19 import pytest
20
21 from vcsserver import hgcompat, hgpatches
22
23
24 LARGEFILES_CAPABILITY = 'largefiles=serve'
25
26
27 def test_patch_largefiles_capabilities_applies_patch(
28 patched_capabilities):
29 lfproto = hgcompat.largefiles.proto
30 hgpatches.patch_largefiles_capabilities()
31 assert lfproto.capabilities.func_name == '_dynamic_capabilities'
32
33
34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
36 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
37 hgcompat.largefiles.proto, stub_extensions)
38
39 caps = dynamic_capabilities(stub_repo, stub_proto)
40
41 stub_extensions.assert_called_once_with(stub_ui)
42 assert LARGEFILES_CAPABILITY not in caps
43
44
45 def test_dynamic_capabilities_uses_updated_capabilitiesorig(
46 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
47 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
48 hgcompat.largefiles.proto, stub_extensions)
49
50 # This happens when the extension is loaded for the first time, important
51 # to ensure that an updated function is correctly picked up.
52 hgcompat.largefiles.proto.capabilitiesorig = mock.Mock(
53 return_value='REPLACED')
54
55 caps = dynamic_capabilities(stub_repo, stub_proto)
56 assert 'REPLACED' == caps
57
58
59 def test_dynamic_capabilities_ignores_updated_capabilities(
60 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
61 stub_extensions.return_value = [('largefiles', mock.Mock())]
62 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
63 hgcompat.largefiles.proto, stub_extensions)
64
65 # This happens when the extension is loaded for the first time, important
66 # to ensure that an updated function is correctly picked up.
67 hgcompat.largefiles.proto.capabilities = mock.Mock(
68 side_effect=Exception('Must not be called'))
69
70 dynamic_capabilities(stub_repo, stub_proto)
71
72
73 def test_dynamic_capabilities_uses_largefiles_if_enabled(
74 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
75 stub_extensions.return_value = [('largefiles', mock.Mock())]
76
77 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
78 hgcompat.largefiles.proto, stub_extensions)
79
80 caps = dynamic_capabilities(stub_repo, stub_proto)
81
82 stub_extensions.assert_called_once_with(stub_ui)
83 assert LARGEFILES_CAPABILITY in caps
84
85
86 @pytest.fixture
87 def patched_capabilities(request):
88 """
89 Patch in `capabilitiesorig` and restore both capability functions.
90 """
91 lfproto = hgcompat.largefiles.proto
92 orig_capabilities = lfproto.capabilities
93 orig_capabilitiesorig = lfproto.capabilitiesorig
94
95 lfproto.capabilitiesorig = mock.Mock(return_value='ORIG')
96
97 @request.addfinalizer
98 def restore():
99 lfproto.capabilities = orig_capabilities
100 lfproto.capabilitiesorig = orig_capabilitiesorig
101
102
103 @pytest.fixture
104 def stub_repo(stub_ui):
105 repo = mock.Mock()
106 repo.ui = stub_ui
107 return repo
108
109
110 @pytest.fixture
111 def stub_proto(stub_ui):
112 proto = mock.Mock()
113 proto.ui = stub_ui
114 return proto
115
116
117 @pytest.fixture
118 def stub_ui():
119 return hgcompat.ui.ui()
120
121
122 @pytest.fixture
123 def stub_extensions():
124 extensions = mock.Mock(return_value=tuple())
125 return extensions
This diff has been collapsed as it changes many lines, (549 lines changed) Show them Hide them
@@ -0,0 +1,549 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import contextlib
19 import io
20 import threading
21 from BaseHTTPServer import BaseHTTPRequestHandler
22 from SocketServer import TCPServer
23
24 import mercurial.ui
25 import mock
26 import pytest
27 import simplejson as json
28
29 from vcsserver import hooks
30
31
32 class HooksStub(object):
33 """
34 Simulates a Proy4.Proxy object.
35
36 Will always return `result`, no matter which hook has been called on it.
37 """
38
39 def __init__(self, result):
40 self._result = result
41
42 def __call__(self, hooks_uri):
43 return self
44
45 def __enter__(self):
46 return self
47
48 def __exit__(self, exc_type, exc_value, traceback):
49 pass
50
51 def __getattr__(self, name):
52 return mock.Mock(return_value=self._result)
53
54
55 @contextlib.contextmanager
56 def mock_hook_response(
57 status=0, output='', exception=None, exception_args=None):
58 response = {
59 'status': status,
60 'output': output,
61 }
62 if exception:
63 response.update({
64 'exception': exception,
65 'exception_args': exception_args,
66 })
67
68 with mock.patch('Pyro4.Proxy', HooksStub(response)):
69 yield
70
71
72 def get_hg_ui(extras=None):
73 """Create a Config object with a valid RC_SCM_DATA entry."""
74 extras = extras or {}
75 required_extras = {
76 'username': '',
77 'repository': '',
78 'locked_by': '',
79 'scm': '',
80 'make_lock': '',
81 'action': '',
82 'ip': '',
83 'hooks_uri': 'fake_hooks_uri',
84 }
85 required_extras.update(extras)
86 hg_ui = mercurial.ui.ui()
87 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
88
89 return hg_ui
90
91
92 def test_call_hook_no_error(capsys):
93 extras = {
94 'hooks_uri': 'fake_hook_uri',
95 }
96 expected_output = 'My mock outptut'
97 writer = mock.Mock()
98
99 with mock_hook_response(status=1, output=expected_output):
100 hooks._call_hook('hook_name', extras, writer)
101
102 out, err = capsys.readouterr()
103
104 writer.write.assert_called_with(expected_output)
105 assert err == ''
106
107
108 def test_call_hook_with_exception(capsys):
109 extras = {
110 'hooks_uri': 'fake_hook_uri',
111 }
112 expected_output = 'My mock outptut'
113 writer = mock.Mock()
114
115 with mock_hook_response(status=1, output=expected_output,
116 exception='TypeError',
117 exception_args=('Mock exception', )):
118 with pytest.raises(Exception) as excinfo:
119 hooks._call_hook('hook_name', extras, writer)
120
121 assert excinfo.type == Exception
122 assert 'Mock exception' in str(excinfo.value)
123
124 out, err = capsys.readouterr()
125
126 writer.write.assert_called_with(expected_output)
127 assert err == ''
128
129
130 def test_call_hook_with_locked_exception(capsys):
131 extras = {
132 'hooks_uri': 'fake_hook_uri',
133 }
134 expected_output = 'My mock outptut'
135 writer = mock.Mock()
136
137 with mock_hook_response(status=1, output=expected_output,
138 exception='HTTPLockedRC',
139 exception_args=('message',)):
140 with pytest.raises(Exception) as excinfo:
141 hooks._call_hook('hook_name', extras, writer)
142
143 assert excinfo.value._vcs_kind == 'repo_locked'
144 assert 'message' == str(excinfo.value)
145
146 out, err = capsys.readouterr()
147
148 writer.write.assert_called_with(expected_output)
149 assert err == ''
150
151
152 def test_call_hook_with_stdout():
153 extras = {
154 'hooks_uri': 'fake_hook_uri',
155 }
156 expected_output = 'My mock outptut'
157
158 stdout = io.BytesIO()
159 with mock_hook_response(status=1, output=expected_output):
160 hooks._call_hook('hook_name', extras, stdout)
161
162 assert stdout.getvalue() == expected_output
163
164
165 def test_repo_size():
166 hg_ui = get_hg_ui()
167
168 with mock_hook_response(status=1):
169 assert hooks.repo_size(hg_ui, None) == 1
170
171
172 def test_pre_pull():
173 hg_ui = get_hg_ui()
174
175 with mock_hook_response(status=1):
176 assert hooks.pre_pull(hg_ui, None) == 1
177
178
179 def test_post_pull():
180 hg_ui = get_hg_ui()
181
182 with mock_hook_response(status=1):
183 assert hooks.post_pull(hg_ui, None) == 1
184
185
186 def test_pre_push():
187 hg_ui = get_hg_ui()
188
189 with mock_hook_response(status=1):
190 assert hooks.pre_push(hg_ui, None) == 1
191
192
193 def test_post_push():
194 hg_ui = get_hg_ui()
195
196 with mock_hook_response(status=1):
197 with mock.patch('vcsserver.hooks._rev_range_hash', return_value=[]):
198 assert hooks.post_push(hg_ui, None, None) == 1
199
200
201 def test_git_pre_receive():
202 extras = {
203 'hooks': ['push'],
204 'hooks_uri': 'fake_hook_uri',
205 }
206 with mock_hook_response(status=1):
207 response = hooks.git_pre_receive(None, None,
208 {'RC_SCM_DATA': json.dumps(extras)})
209 assert response == 1
210
211
212 def test_git_pre_receive_is_disabled():
213 extras = {'hooks': ['pull']}
214 response = hooks.git_pre_receive(None, None,
215 {'RC_SCM_DATA': json.dumps(extras)})
216
217 assert response == 0
218
219
220 def test_git_post_receive_no_subprocess_call():
221 extras = {
222 'hooks': ['push'],
223 'hooks_uri': 'fake_hook_uri',
224 }
225 # Setting revision_lines to '' avoid all subprocess_calls
226 with mock_hook_response(status=1):
227 response = hooks.git_post_receive(None, '',
228 {'RC_SCM_DATA': json.dumps(extras)})
229 assert response == 1
230
231
232 def test_git_post_receive_is_disabled():
233 extras = {'hooks': ['pull']}
234 response = hooks.git_post_receive(None, '',
235 {'RC_SCM_DATA': json.dumps(extras)})
236
237 assert response == 0
238
239
240 def test_git_post_receive_calls_repo_size():
241 extras = {'hooks': ['push', 'repo_size']}
242 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
243 hooks.git_post_receive(
244 None, '', {'RC_SCM_DATA': json.dumps(extras)})
245 extras.update({'commit_ids': []})
246 expected_calls = [
247 mock.call('repo_size', extras, mock.ANY),
248 mock.call('post_push', extras, mock.ANY),
249 ]
250 assert call_hook_mock.call_args_list == expected_calls
251
252
253 def test_git_post_receive_does_not_call_disabled_repo_size():
254 extras = {'hooks': ['push']}
255 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
256 hooks.git_post_receive(
257 None, '', {'RC_SCM_DATA': json.dumps(extras)})
258 extras.update({'commit_ids': []})
259 expected_calls = [
260 mock.call('post_push', extras, mock.ANY)
261 ]
262 assert call_hook_mock.call_args_list == expected_calls
263
264
265 def test_repo_size_exception_does_not_affect_git_post_receive():
266 extras = {'hooks': ['push', 'repo_size']}
267 status = 0
268
269 def side_effect(name, *args, **kwargs):
270 if name == 'repo_size':
271 raise Exception('Fake exception')
272 else:
273 return status
274
275 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
276 call_hook_mock.side_effect = side_effect
277 result = hooks.git_post_receive(
278 None, '', {'RC_SCM_DATA': json.dumps(extras)})
279 assert result == status
280
281
282 @mock.patch('vcsserver.hooks._run_command')
283 def test_git_post_receive_first_commit_sub_branch(cmd_mock):
284 def cmd_mock_returns(args):
285 if args == ['git', 'show', 'HEAD']:
286 raise
287 if args == ['git', 'for-each-ref', '--format=%(refname)',
288 'refs/heads/*']:
289 return 'refs/heads/test-branch2/sub-branch'
290 if args == ['git', 'log', '--reverse', '--pretty=format:%H', '--',
291 '9695eef57205c17566a3ae543be187759b310bb7', '--not',
292 'refs/heads/test-branch2/sub-branch']:
293 return ''
294
295 cmd_mock.side_effect = cmd_mock_returns
296
297 extras = {
298 'hooks': ['push'],
299 'hooks_uri': 'fake_hook_uri'
300 }
301 rev_lines = ['0000000000000000000000000000000000000000 '
302 '9695eef57205c17566a3ae543be187759b310bb7 '
303 'refs/heads/feature/sub-branch\n']
304 with mock_hook_response(status=0):
305 response = hooks.git_post_receive(None, rev_lines,
306 {'RC_SCM_DATA': json.dumps(extras)})
307
308 calls = [
309 mock.call(['git', 'show', 'HEAD']),
310 mock.call(['git', 'symbolic-ref', 'HEAD',
311 'refs/heads/feature/sub-branch']),
312 ]
313 cmd_mock.assert_has_calls(calls, any_order=True)
314 assert response == 0
315
316
317 @mock.patch('vcsserver.hooks._run_command')
318 def test_git_post_receive_first_commit_revs(cmd_mock):
319 extras = {
320 'hooks': ['push'],
321 'hooks_uri': 'fake_hook_uri'
322 }
323 rev_lines = [
324 '0000000000000000000000000000000000000000 '
325 '9695eef57205c17566a3ae543be187759b310bb7 refs/heads/master\n']
326 with mock_hook_response(status=0):
327 response = hooks.git_post_receive(
328 None, rev_lines, {'RC_SCM_DATA': json.dumps(extras)})
329
330 calls = [
331 mock.call(['git', 'show', 'HEAD']),
332 mock.call(['git', 'for-each-ref', '--format=%(refname)',
333 'refs/heads/*']),
334 mock.call(['git', 'log', '--reverse', '--pretty=format:%H',
335 '--', '9695eef57205c17566a3ae543be187759b310bb7', '--not',
336 ''])
337 ]
338 cmd_mock.assert_has_calls(calls, any_order=True)
339
340 assert response == 0
341
342
343 def test_git_pre_pull():
344 extras = {
345 'hooks': ['pull'],
346 'hooks_uri': 'fake_hook_uri',
347 }
348 with mock_hook_response(status=1, output='foo'):
349 assert hooks.git_pre_pull(extras) == hooks.HookResponse(1, 'foo')
350
351
352 def test_git_pre_pull_exception_is_caught():
353 extras = {
354 'hooks': ['pull'],
355 'hooks_uri': 'fake_hook_uri',
356 }
357 with mock_hook_response(status=2, exception=Exception('foo')):
358 assert hooks.git_pre_pull(extras).status == 128
359
360
361 def test_git_pre_pull_is_disabled():
362 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
363
364
365 def test_git_post_pull():
366 extras = {
367 'hooks': ['pull'],
368 'hooks_uri': 'fake_hook_uri',
369 }
370 with mock_hook_response(status=1, output='foo'):
371 assert hooks.git_post_pull(extras) == hooks.HookResponse(1, 'foo')
372
373
374 def test_git_post_pull_exception_is_caught():
375 extras = {
376 'hooks': ['pull'],
377 'hooks_uri': 'fake_hook_uri',
378 }
379 with mock_hook_response(status=2, exception='Exception',
380 exception_args=('foo',)):
381 assert hooks.git_post_pull(extras).status == 128
382
383
384 def test_git_post_pull_is_disabled():
385 assert (
386 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
387
388
389 class TestGetHooksClient(object):
390 def test_returns_pyro_client_when_protocol_matches(self):
391 hooks_uri = 'localhost:8000'
392 result = hooks._get_hooks_client({
393 'hooks_uri': hooks_uri,
394 'hooks_protocol': 'pyro4'
395 })
396 assert isinstance(result, hooks.HooksPyro4Client)
397 assert result.hooks_uri == hooks_uri
398
399 def test_returns_http_client_when_protocol_matches(self):
400 hooks_uri = 'localhost:8000'
401 result = hooks._get_hooks_client({
402 'hooks_uri': hooks_uri,
403 'hooks_protocol': 'http'
404 })
405 assert isinstance(result, hooks.HooksHttpClient)
406 assert result.hooks_uri == hooks_uri
407
408 def test_returns_pyro4_client_when_no_protocol_is_specified(self):
409 hooks_uri = 'localhost:8000'
410 result = hooks._get_hooks_client({
411 'hooks_uri': hooks_uri
412 })
413 assert isinstance(result, hooks.HooksPyro4Client)
414 assert result.hooks_uri == hooks_uri
415
416 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
417 fake_module = mock.Mock()
418 import_patcher = mock.patch.object(
419 hooks.importlib, 'import_module', return_value=fake_module)
420 fake_module_name = 'fake.module'
421 with import_patcher as import_mock:
422 result = hooks._get_hooks_client(
423 {'hooks_module': fake_module_name})
424
425 import_mock.assert_called_once_with(fake_module_name)
426 assert isinstance(result, hooks.HooksDummyClient)
427 assert result._hooks_module == fake_module
428
429
430 class TestHooksHttpClient(object):
431 def test_init_sets_hooks_uri(self):
432 uri = 'localhost:3000'
433 client = hooks.HooksHttpClient(uri)
434 assert client.hooks_uri == uri
435
436 def test_serialize_returns_json_string(self):
437 client = hooks.HooksHttpClient('localhost:3000')
438 hook_name = 'test'
439 extras = {
440 'first': 1,
441 'second': 'two'
442 }
443 result = client._serialize(hook_name, extras)
444 expected_result = json.dumps({
445 'method': hook_name,
446 'extras': extras
447 })
448 assert result == expected_result
449
450 def test_call_queries_http_server(self, http_mirror):
451 client = hooks.HooksHttpClient(http_mirror.uri)
452 hook_name = 'test'
453 extras = {
454 'first': 1,
455 'second': 'two'
456 }
457 result = client(hook_name, extras)
458 expected_result = {
459 'method': hook_name,
460 'extras': extras
461 }
462 assert result == expected_result
463
464
465 class TestHooksDummyClient(object):
466 def test_init_imports_hooks_module(self):
467 hooks_module_name = 'rhodecode.fake.module'
468 hooks_module = mock.MagicMock()
469
470 import_patcher = mock.patch.object(
471 hooks.importlib, 'import_module', return_value=hooks_module)
472 with import_patcher as import_mock:
473 client = hooks.HooksDummyClient(hooks_module_name)
474 import_mock.assert_called_once_with(hooks_module_name)
475 assert client._hooks_module == hooks_module
476
477 def test_call_returns_hook_result(self):
478 hooks_module_name = 'rhodecode.fake.module'
479 hooks_module = mock.MagicMock()
480 import_patcher = mock.patch.object(
481 hooks.importlib, 'import_module', return_value=hooks_module)
482 with import_patcher:
483 client = hooks.HooksDummyClient(hooks_module_name)
484
485 result = client('post_push', {})
486 hooks_module.Hooks.assert_called_once_with()
487 assert result == hooks_module.Hooks().__enter__().post_push()
488
489
490 class TestHooksPyro4Client(object):
491 def test_init_sets_hooks_uri(self):
492 uri = 'localhost:3000'
493 client = hooks.HooksPyro4Client(uri)
494 assert client.hooks_uri == uri
495
496 def test_call_returns_hook_value(self):
497 hooks_uri = 'localhost:3000'
498 client = hooks.HooksPyro4Client(hooks_uri)
499 hooks_module = mock.Mock()
500 context_manager = mock.MagicMock()
501 context_manager.__enter__.return_value = hooks_module
502 pyro4_patcher = mock.patch.object(
503 hooks.Pyro4, 'Proxy', return_value=context_manager)
504 extras = {
505 'test': 'test'
506 }
507 with pyro4_patcher as pyro4_mock:
508 result = client('post_push', extras)
509 pyro4_mock.assert_called_once_with(hooks_uri)
510 hooks_module.post_push.assert_called_once_with(extras)
511 assert result == hooks_module.post_push.return_value
512
513
514 @pytest.fixture
515 def http_mirror(request):
516 server = MirrorHttpServer()
517 request.addfinalizer(server.stop)
518 return server
519
520
521 class MirrorHttpHandler(BaseHTTPRequestHandler):
522 def do_POST(self):
523 length = int(self.headers['Content-Length'])
524 body = self.rfile.read(length).decode('utf-8')
525 self.send_response(200)
526 self.end_headers()
527 self.wfile.write(body)
528
529
530 class MirrorHttpServer(object):
531 ip_address = '127.0.0.1'
532 port = 0
533
534 def __init__(self):
535 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
536 _, self.port = self._daemon.server_address
537 self._thread = threading.Thread(target=self._daemon.serve_forever)
538 self._thread.daemon = True
539 self._thread.start()
540
541 def stop(self):
542 self._daemon.shutdown()
543 self._thread.join()
544 self._daemon = None
545 self._thread = None
546
547 @property
548 def uri(self):
549 return '{}:{}'.format(self.ip_address, self.port)
@@ -0,0 +1,44 b''
1 """
2 Tests used to profile the HTTP based implementation.
3 """
4
5 import pytest
6 import webtest
7
8 from vcsserver.http_main import main
9
10
11 @pytest.fixture
12 def vcs_app():
13 stub_settings = {
14 'dev.use_echo_app': 'true',
15 'beaker.cache.regions': 'repo_object',
16 'beaker.cache.repo_object.type': 'memorylru',
17 'beaker.cache.repo_object.max_items': '100',
18 'beaker.cache.repo_object.expire': '300',
19 'beaker.cache.repo_object.enabled': 'true',
20 'locale': 'en_US.UTF-8',
21 }
22 vcs_app = main({}, **stub_settings)
23 app = webtest.TestApp(vcs_app)
24 return app
25
26
27 @pytest.fixture(scope='module')
28 def data():
29 one_kb = 'x' * 1024
30 return one_kb * 1024 * 10
31
32
33 def test_http_app_streaming_with_data(data, repeat, vcs_app):
34 app = vcs_app
35 for x in xrange(repeat / 10):
36 response = app.post('/stream/git/', params=data)
37 assert response.status_code == 200
38
39
40 def test_http_app_streaming_no_data(repeat, vcs_app):
41 app = vcs_app
42 for x in xrange(repeat / 10):
43 response = app.post('/stream/git/')
44 assert response.status_code == 200
@@ -0,0 +1,36 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import mock
19
20 from vcsserver import main
21
22
23 @mock.patch('vcsserver.main.VcsServerCommand', mock.Mock())
24 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
25 def test_applies_largefiles_patch(patch_largefiles_capabilities):
26 main.main([])
27 patch_largefiles_capabilities.assert_called_once_with()
28
29
30 @mock.patch('vcsserver.main.VcsServerCommand', mock.Mock())
31 @mock.patch('vcsserver.main.MercurialFactory', None)
32 @mock.patch(
33 'vcsserver.hgpatches.patch_largefiles_capabilities',
34 mock.Mock(side_effect=Exception("Must not be called")))
35 def test_applies_largefiles_patch_only_if_mercurial_is_available():
36 main.main([])
@@ -0,0 +1,249 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import io
19
20 import dulwich.protocol
21 import mock
22 import pytest
23 import webob
24 import webtest
25
26 from vcsserver import hooks, pygrack
27
28 # pylint: disable=redefined-outer-name,protected-access
29
30
31 @pytest.fixture()
32 def pygrack_instance(tmpdir):
33 """
34 Creates a pygrack app instance.
35
36 Right now, it does not much helpful regarding the passed directory.
37 It just contains the required folders to pass the signature test.
38 """
39 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
40 tmpdir.mkdir(dir_name)
41
42 return pygrack.GitRepository('repo_name', str(tmpdir), 'git', False, {})
43
44
45 @pytest.fixture()
46 def pygrack_app(pygrack_instance):
47 """
48 Creates a pygrack app wrapped in webtest.TestApp.
49 """
50 return webtest.TestApp(pygrack_instance)
51
52
53 def test_invalid_service_info_refs_returns_403(pygrack_app):
54 response = pygrack_app.get('/info/refs?service=git-upload-packs',
55 expect_errors=True)
56
57 assert response.status_int == 403
58
59
60 def test_invalid_endpoint_returns_403(pygrack_app):
61 response = pygrack_app.post('/git-upload-packs', expect_errors=True)
62
63 assert response.status_int == 403
64
65
66 @pytest.mark.parametrize('sideband', [
67 'side-band-64k',
68 'side-band',
69 'side-band no-progress',
70 ])
71 def test_pre_pull_hook_fails_with_sideband(pygrack_app, sideband):
72 request = ''.join([
73 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ',
74 'multi_ack %s ofs-delta\n' % sideband,
75 '0000',
76 '0009done\n',
77 ])
78 with mock.patch('vcsserver.hooks.git_pre_pull',
79 return_value=hooks.HookResponse(1, 'foo')):
80 response = pygrack_app.post(
81 '/git-upload-pack', params=request,
82 content_type='application/x-git-upload-pack')
83
84 data = io.BytesIO(response.body)
85 proto = dulwich.protocol.Protocol(data.read, None)
86 packets = list(proto.read_pkt_seq())
87
88 expected_packets = [
89 'NAK\n', '\x02foo', '\x02Pre pull hook failed: aborting\n',
90 '\x01' + pygrack.GitRepository.EMPTY_PACK,
91 ]
92 assert packets == expected_packets
93
94
95 def test_pre_pull_hook_fails_no_sideband(pygrack_app):
96 request = ''.join([
97 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
98 'multi_ack ofs-delta\n'
99 '0000',
100 '0009done\n',
101 ])
102 with mock.patch('vcsserver.hooks.git_pre_pull',
103 return_value=hooks.HookResponse(1, 'foo')):
104 response = pygrack_app.post(
105 '/git-upload-pack', params=request,
106 content_type='application/x-git-upload-pack')
107
108 assert response.body == pygrack.GitRepository.EMPTY_PACK
109
110
111 def test_pull_has_hook_messages(pygrack_app):
112 request = ''.join([
113 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
114 'multi_ack side-band-64k ofs-delta\n'
115 '0000',
116 '0009done\n',
117 ])
118 with mock.patch('vcsserver.hooks.git_pre_pull',
119 return_value=hooks.HookResponse(0, 'foo')):
120 with mock.patch('vcsserver.hooks.git_post_pull',
121 return_value=hooks.HookResponse(1, 'bar')):
122 with mock.patch('vcsserver.subprocessio.SubprocessIOChunker',
123 return_value=['0008NAK\n0009subp\n0000']):
124 response = pygrack_app.post(
125 '/git-upload-pack', params=request,
126 content_type='application/x-git-upload-pack')
127
128 data = io.BytesIO(response.body)
129 proto = dulwich.protocol.Protocol(data.read, None)
130 packets = list(proto.read_pkt_seq())
131
132 assert packets == ['NAK\n', '\x02foo', 'subp\n', '\x02bar']
133
134
135 def test_get_want_capabilities(pygrack_instance):
136 data = io.BytesIO(
137 '0054want 74730d410fcb6603ace96f1dc55ea6196122532d ' +
138 'multi_ack side-band-64k ofs-delta\n00000009done\n')
139
140 request = webob.Request({
141 'wsgi.input': data,
142 'REQUEST_METHOD': 'POST',
143 'webob.is_body_seekable': True
144 })
145
146 capabilities = pygrack_instance._get_want_capabilities(request)
147
148 assert capabilities == frozenset(
149 ('ofs-delta', 'multi_ack', 'side-band-64k'))
150 assert data.tell() == 0
151
152
153 @pytest.mark.parametrize('data,capabilities,expected', [
154 ('foo', [], []),
155 ('', ['side-band-64k'], []),
156 ('', ['side-band'], []),
157 ('foo', ['side-band-64k'], ['0008\x02foo']),
158 ('foo', ['side-band'], ['0008\x02foo']),
159 ('f'*1000, ['side-band-64k'], ['03ed\x02' + 'f' * 1000]),
160 ('f'*1000, ['side-band'], ['03e8\x02' + 'f' * 995, '000a\x02fffff']),
161 ('f'*65520, ['side-band-64k'], ['fff0\x02' + 'f' * 65515, '000a\x02fffff']),
162 ('f'*65520, ['side-band'], ['03e8\x02' + 'f' * 995] * 65 + ['0352\x02' + 'f' * 845]),
163 ], ids=[
164 'foo-empty',
165 'empty-64k', 'empty',
166 'foo-64k', 'foo',
167 'f-1000-64k', 'f-1000',
168 'f-65520-64k', 'f-65520'])
169 def test_get_messages(pygrack_instance, data, capabilities, expected):
170 messages = pygrack_instance._get_messages(data, capabilities)
171
172 assert messages == expected
173
174
175 @pytest.mark.parametrize('response,capabilities,pre_pull_messages,post_pull_messages', [
176 # Unexpected response
177 ('unexpected_response', ['side-band-64k'], 'foo', 'bar'),
178 # No sideband
179 ('no-sideband', [], 'foo', 'bar'),
180 # No messages
181 ('no-messages', ['side-band-64k'], '', ''),
182 ])
183 def test_inject_messages_to_response_nothing_to_do(
184 pygrack_instance, response, capabilities, pre_pull_messages,
185 post_pull_messages):
186 new_response = pygrack_instance._inject_messages_to_response(
187 response, capabilities, pre_pull_messages, post_pull_messages)
188
189 assert new_response == response
190
191
192 @pytest.mark.parametrize('capabilities', [
193 ['side-band'],
194 ['side-band-64k'],
195 ])
196 def test_inject_messages_to_response_single_element(pygrack_instance,
197 capabilities):
198 response = ['0008NAK\n0009subp\n0000']
199 new_response = pygrack_instance._inject_messages_to_response(
200 response, capabilities, 'foo', 'bar')
201
202 expected_response = [
203 '0008NAK\n', '0008\x02foo', '0009subp\n', '0008\x02bar', '0000']
204
205 assert new_response == expected_response
206
207
208 @pytest.mark.parametrize('capabilities', [
209 ['side-band'],
210 ['side-band-64k'],
211 ])
212 def test_inject_messages_to_response_multi_element(pygrack_instance,
213 capabilities):
214 response = [
215 '0008NAK\n000asubp1\n', '000asubp2\n', '000asubp3\n', '000asubp4\n0000']
216 new_response = pygrack_instance._inject_messages_to_response(
217 response, capabilities, 'foo', 'bar')
218
219 expected_response = [
220 '0008NAK\n', '0008\x02foo', '000asubp1\n', '000asubp2\n', '000asubp3\n',
221 '000asubp4\n', '0008\x02bar', '0000'
222 ]
223
224 assert new_response == expected_response
225
226
227 def test_build_failed_pre_pull_response_no_sideband(pygrack_instance):
228 response = pygrack_instance._build_failed_pre_pull_response([], 'foo')
229
230 assert response == [pygrack.GitRepository.EMPTY_PACK]
231
232
233 @pytest.mark.parametrize('capabilities', [
234 ['side-band'],
235 ['side-band-64k'],
236 ['side-band-64k', 'no-progress'],
237 ])
238 def test_build_failed_pre_pull_response(pygrack_instance, capabilities):
239 response = pygrack_instance._build_failed_pre_pull_response(
240 capabilities, 'foo')
241
242 expected_response = [
243 '0008NAK\n', '0008\x02foo', '0024\x02Pre pull hook failed: aborting\n',
244 '%04x\x01%s' % (len(pygrack.GitRepository.EMPTY_PACK) + 5,
245 pygrack.GitRepository.EMPTY_PACK),
246 '0000',
247 ]
248
249 assert response == expected_response
@@ -0,0 +1,86 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import os
19
20 import mercurial.hg
21 import mercurial.ui
22 import mercurial.error
23 import mock
24 import pytest
25 import webtest
26
27 from vcsserver import scm_app
28
29
30 def test_hg_does_not_accept_invalid_cmd(tmpdir):
31 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
32 app = webtest.TestApp(scm_app.HgWeb(repo))
33
34 response = app.get('/repo?cmd=invalidcmd', expect_errors=True)
35
36 assert response.status_int == 400
37
38
39 def test_create_hg_wsgi_app_requirement_error(tmpdir):
40 repo = mercurial.hg.repository(mercurial.ui.ui(), str(tmpdir), create=True)
41 config = (
42 ('paths', 'default', ''),
43 )
44 with mock.patch('vcsserver.scm_app.HgWeb') as hgweb_mock:
45 hgweb_mock.side_effect = mercurial.error.RequirementError()
46 with pytest.raises(Exception):
47 scm_app.create_hg_wsgi_app(str(tmpdir), repo, config)
48
49
50 def test_git_returns_not_found(tmpdir):
51 app = webtest.TestApp(
52 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
53
54 response = app.get('/repo_name/inforefs?service=git-upload-pack',
55 expect_errors=True)
56
57 assert response.status_int == 404
58
59
60 def test_git(tmpdir):
61 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
62 tmpdir.mkdir(dir_name)
63
64 app = webtest.TestApp(
65 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
66
67 # We set service to git-upload-packs to trigger a 403
68 response = app.get('/repo_name/inforefs?service=git-upload-packs',
69 expect_errors=True)
70
71 assert response.status_int == 403
72
73
74 def test_git_fallbacks_to_git_folder(tmpdir):
75 tmpdir.mkdir('.git')
76 for dir_name in ('config', 'head', 'info', 'objects', 'refs'):
77 tmpdir.mkdir(os.path.join('.git', dir_name))
78
79 app = webtest.TestApp(
80 scm_app.GitHandler(str(tmpdir), 'repo_name', 'git', False, {}))
81
82 # We set service to git-upload-packs to trigger a 403
83 response = app.get('/repo_name/inforefs?service=git-upload-packs',
84 expect_errors=True)
85
86 assert response.status_int == 403
@@ -0,0 +1,39 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import os
19
20 import mock
21 import pytest
22
23 from vcsserver.server import VcsServer
24
25
26 def test_provides_the_pid(server):
27 pid = server.get_pid()
28 assert pid == os.getpid()
29
30
31 def test_allows_to_trigger_the_garbage_collector(server):
32 with mock.patch('gc.collect') as collect:
33 server.run_gc()
34 assert collect.called
35
36
37 @pytest.fixture
38 def server():
39 return VcsServer()
@@ -0,0 +1,122 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import io
19 import os
20 import sys
21
22 import pytest
23
24 from vcsserver import subprocessio
25
26
27 @pytest.fixture(scope='module')
28 def environ():
29 """Delete coverage variables, as they make the tests fail."""
30 env = dict(os.environ)
31 for key in env.keys():
32 if key.startswith('COV_CORE_'):
33 del env[key]
34
35 return env
36
37
38 def _get_python_args(script):
39 return [sys.executable, '-c',
40 'import sys; import time; import shutil; ' + script]
41
42
43 def test_raise_exception_on_non_zero_return_code(environ):
44 args = _get_python_args('sys.exit(1)')
45 with pytest.raises(EnvironmentError):
46 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
47
48
49 def test_does_not_fail_on_non_zero_return_code(environ):
50 args = _get_python_args('sys.exit(1)')
51 output = ''.join(subprocessio.SubprocessIOChunker(
52 args, shell=False, fail_on_return_code=False, env=environ))
53
54 assert output == ''
55
56
57 def test_raise_exception_on_stderr(environ):
58 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
59 with pytest.raises(EnvironmentError) as excinfo:
60 list(subprocessio.SubprocessIOChunker(args, shell=False, env=environ))
61
62 assert 'exited due to an error:\nX' in str(excinfo.value)
63
64
65 def test_does_not_fail_on_stderr(environ):
66 args = _get_python_args('sys.stderr.write("X"); time.sleep(1);')
67 output = ''.join(subprocessio.SubprocessIOChunker(
68 args, shell=False, fail_on_stderr=False, env=environ))
69
70 assert output == ''
71
72
73 @pytest.mark.parametrize('size', [1, 10**5])
74 def test_output_with_no_input(size, environ):
75 print type(environ)
76 data = 'X'
77 args = _get_python_args('sys.stdout.write("%s" * %d)' % (data, size))
78 output = ''.join(subprocessio.SubprocessIOChunker(
79 args, shell=False, env=environ))
80
81 assert output == data * size
82
83
84 @pytest.mark.parametrize('size', [1, 10**5])
85 def test_output_with_no_input_does_not_fail(size, environ):
86 data = 'X'
87 args = _get_python_args(
88 'sys.stdout.write("%s" * %d); sys.exit(1)' % (data, size))
89 output = ''.join(subprocessio.SubprocessIOChunker(
90 args, shell=False, fail_on_return_code=False, env=environ))
91
92 print len(data * size), len(output)
93 assert output == data * size
94
95
96 @pytest.mark.parametrize('size', [1, 10**5])
97 def test_output_with_input(size, environ):
98 data = 'X' * size
99 inputstream = io.BytesIO(data)
100 # This acts like the cat command.
101 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
102 output = ''.join(subprocessio.SubprocessIOChunker(
103 args, shell=False, inputstream=inputstream, env=environ))
104
105 print len(data), len(output)
106 assert output == data
107
108
109 @pytest.mark.parametrize('size', [1, 10**5])
110 def test_output_with_input_skipping_iterator(size, environ):
111 data = 'X' * size
112 inputstream = io.BytesIO(data)
113 # This acts like the cat command.
114 args = _get_python_args('shutil.copyfileobj(sys.stdin, sys.stdout)')
115
116 # Note: assigning the chunker makes sure that it is not deleted too early
117 chunker = subprocessio.SubprocessIOChunker(
118 args, shell=False, inputstream=inputstream, env=environ)
119 output = ''.join(chunker.output)
120
121 print len(data), len(output)
122 assert output == data
@@ -0,0 +1,67 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import io
19 import mock
20 import pytest
21 import sys
22
23
24 class MockPopen(object):
25 def __init__(self, stderr):
26 self.stdout = io.BytesIO('')
27 self.stderr = io.BytesIO(stderr)
28 self.returncode = 1
29
30 def wait(self):
31 pass
32
33
34 INVALID_CERTIFICATE_STDERR = '\n'.join([
35 'svnrdump: E230001: Unable to connect to a repository at URL url',
36 'svnrdump: E230001: Server SSL certificate verification failed: issuer is not trusted',
37 ])
38
39
40 @pytest.mark.parametrize('stderr,expected_reason', [
41 (INVALID_CERTIFICATE_STDERR, 'INVALID_CERTIFICATE'),
42 ('svnrdump: E123456', 'UNKNOWN'),
43 ])
44 @pytest.mark.xfail(sys.platform == "cygwin",
45 reason="SVN not packaged for Cygwin")
46 def test_import_remote_repository_certificate_error(stderr, expected_reason):
47 from vcsserver import svn
48
49 remote = svn.SvnRemote(None)
50 remote.is_path_valid_repository = lambda wire, path: True
51
52 with mock.patch('subprocess.Popen',
53 return_value=MockPopen(stderr)):
54 with pytest.raises(Exception) as excinfo:
55 remote.import_remote_repository({'path': 'path'}, 'url')
56
57 expected_error_args = (
58 'Failed to dump the remote repository from url.',
59 expected_reason)
60
61 assert excinfo.value.args == expected_error_args
62
63
64 def test_svn_libraries_can_be_imported():
65 import svn
66 import svn.client
67 assert svn.client is not None
@@ -0,0 +1,132 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import subprocess
19 import StringIO
20 import time
21
22 import pytest
23
24 from fixture import TestINI
25
26
27 @pytest.mark.parametrize("arguments, expected_texts", [
28 (['--threadpool=192'], [
29 'threadpool_size: 192',
30 'worker pool of size 192 created',
31 'Threadpool size set to 192']),
32 (['--locale=fake'], [
33 'Cannot set locale, not configuring the locale system']),
34 (['--timeout=5'], [
35 'Timeout for RPC calls set to 5.0 seconds']),
36 (['--log-level=info'], [
37 'log_level: info']),
38 (['--port={port}'], [
39 'port: {port}',
40 'created daemon on localhost:{port}']),
41 (['--host=127.0.0.1', '--port={port}'], [
42 'port: {port}',
43 'host: 127.0.0.1',
44 'created daemon on 127.0.0.1:{port}']),
45 (['--config=/bad/file'], ['OSError: File /bad/file does not exist']),
46 ])
47 def test_vcsserver_calls(arguments, expected_texts, vcsserver_port):
48 port_argument = '--port={port}'
49 if port_argument not in arguments:
50 arguments.append(port_argument)
51 arguments = _replace_port(arguments, vcsserver_port)
52 expected_texts = _replace_port(expected_texts, vcsserver_port)
53 output = call_vcs_server_with_arguments(arguments)
54 for text in expected_texts:
55 assert text in output
56
57
58 def _replace_port(values, port):
59 return [value.format(port=port) for value in values]
60
61
62 def test_vcsserver_with_config(vcsserver_port):
63 ini_def = [
64 {'DEFAULT': {'host': '127.0.0.1'}},
65 {'DEFAULT': {'threadpool_size': '111'}},
66 {'DEFAULT': {'port': vcsserver_port}},
67 ]
68
69 with TestINI('test.ini', ini_def) as new_test_ini_path:
70 output = call_vcs_server_with_arguments(
71 ['--config=' + new_test_ini_path])
72
73 expected_texts = [
74 'host: 127.0.0.1',
75 'Threadpool size set to 111',
76 ]
77 for text in expected_texts:
78 assert text in output
79
80
81 def test_vcsserver_with_config_cli_overwrite(vcsserver_port):
82 ini_def = [
83 {'DEFAULT': {'host': '127.0.0.1'}},
84 {'DEFAULT': {'port': vcsserver_port}},
85 {'DEFAULT': {'threadpool_size': '111'}},
86 {'DEFAULT': {'timeout': '0'}},
87 ]
88 with TestINI('test.ini', ini_def) as new_test_ini_path:
89 output = call_vcs_server_with_arguments([
90 '--config=' + new_test_ini_path,
91 '--host=128.0.0.1',
92 '--threadpool=256',
93 '--timeout=5'])
94 expected_texts = [
95 'host: 128.0.0.1',
96 'Threadpool size set to 256',
97 'Timeout for RPC calls set to 5.0 seconds',
98 ]
99 for text in expected_texts:
100 assert text in output
101
102
103 def call_vcs_server_with_arguments(args):
104 vcs = subprocess.Popen(
105 ["vcsserver"] + args,
106 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
107
108 output = read_output_until(
109 "Starting vcsserver.main", vcs.stdout)
110 vcs.terminate()
111 return output
112
113
114 def call_vcs_server_with_non_existing_config_file(args):
115 vcs = subprocess.Popen(
116 ["vcsserver", "--config=/tmp/bad"] + args,
117 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
118 output = read_output_until(
119 "Starting vcsserver.main", vcs.stdout)
120 vcs.terminate()
121 return output
122
123
124 def read_output_until(expected, source, timeout=5):
125 ts = time.time()
126 buf = StringIO.StringIO()
127 while time.time() - ts < timeout:
128 line = source.readline()
129 buf.write(line)
130 if expected in line:
131 break
132 return buf.getvalue()
@@ -0,0 +1,96 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import wsgiref.simple_server
19 import wsgiref.validate
20
21 from vcsserver import wsgi_app_caller
22
23
24 # pylint: disable=protected-access,too-many-public-methods
25
26
27 @wsgiref.validate.validator
28 def demo_app(environ, start_response):
29 """WSGI app used for testing."""
30 data = [
31 'Hello World!\n',
32 'input_data=%s\n' % environ['wsgi.input'].read(),
33 ]
34 for key, value in sorted(environ.items()):
35 data.append('%s=%s\n' % (key, value))
36
37 write = start_response("200 OK", [('Content-Type', 'text/plain')])
38 write('Old school write method\n')
39 write('***********************\n')
40 return data
41
42
43 BASE_ENVIRON = {
44 'REQUEST_METHOD': 'GET',
45 'SERVER_NAME': 'localhost',
46 'SERVER_PORT': '80',
47 'SCRIPT_NAME': '',
48 'PATH_INFO': '/',
49 'QUERY_STRING': '',
50 'foo.var': 'bla',
51 }
52
53
54 def test_complete_environ():
55 environ = dict(BASE_ENVIRON)
56 data = "data"
57 wsgi_app_caller._complete_environ(environ, data)
58 wsgiref.validate.check_environ(environ)
59
60 assert data == environ['wsgi.input'].read()
61
62
63 def test_start_response():
64 start_response = wsgi_app_caller._StartResponse()
65 status = '200 OK'
66 headers = [('Content-Type', 'text/plain')]
67 start_response(status, headers)
68
69 assert status == start_response.status
70 assert headers == start_response.headers
71
72
73 def test_start_response_with_error():
74 start_response = wsgi_app_caller._StartResponse()
75 status = '500 Internal Server Error'
76 headers = [('Content-Type', 'text/plain')]
77 start_response(status, headers, (None, None, None))
78
79 assert status == start_response.status
80 assert headers == start_response.headers
81
82
83 def test_wsgi_app_caller():
84 caller = wsgi_app_caller.WSGIAppCaller(demo_app)
85 environ = dict(BASE_ENVIRON)
86 input_data = 'some text'
87 responses, status, headers = caller.handle(environ, input_data)
88 response = ''.join(responses)
89
90 assert status == '200 OK'
91 assert headers == [('Content-Type', 'text/plain')]
92 assert response.startswith(
93 'Old school write method\n***********************\n')
94 assert 'Hello World!\n' in response
95 assert 'foo.var=bla\n' in response
96 assert 'input_data=%s\n' % input_data in response
@@ -0,0 +1,1 b''
1 4.0.0 No newline at end of file
@@ -0,0 +1,21 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import pkgutil
19
20
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip()
@@ -0,0 +1,71 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import logging
19
20
21 log = logging.getLogger(__name__)
22
23
24 class RepoFactory(object):
25 """
26 Utility to create instances of repository
27
28 It provides internal caching of the `repo` object based on
29 the :term:`call context`.
30 """
31
32 def __init__(self, repo_cache):
33 self._cache = repo_cache
34
35 def _create_config(self, path, config):
36 config = {}
37 return config
38
39 def _create_repo(self, wire, create):
40 raise NotImplementedError()
41
42 def repo(self, wire, create=False):
43 """
44 Get a repository instance for the given path.
45
46 Uses internally the low level beaker API since the decorators introduce
47 significant overhead.
48 """
49 def create_new_repo():
50 return self._create_repo(wire, create)
51
52 return self._repo(wire, create_new_repo)
53
54 def _repo(self, wire, createfunc):
55 context = wire.get('context', None)
56 cache = wire.get('cache', True)
57 log.debug(
58 'GET %s@%s with cache:%s. Context: %s',
59 self.__class__.__name__, wire['path'], cache, context)
60
61 if context and cache:
62 cache_key = (context, wire['path'])
63 log.debug(
64 'FETCH %s@%s repo object from cache. Context: %s',
65 self.__class__.__name__, wire['path'], context)
66 return self._cache.get(key=cache_key, createfunc=createfunc)
67 else:
68 log.debug(
69 'INIT %s@%s repo object based on wire %s. Context: %s',
70 self.__class__.__name__, wire['path'], wire, context)
71 return createfunc()
@@ -0,0 +1,8 b''
1 """
2 Provides a stub implementation for VCS operations.
3
4 Intended usage is to help in performance measurements. The basic idea is to
5 implement an `EchoApp` which sends back what it gets. Based on a configuration
6 parameter this app can be activated, so that it replaced the endpoints for Git
7 and Mercurial.
8 """
@@ -0,0 +1,34 b''
1 """
2 Implementation of :class:`EchoApp`.
3
4 This WSGI application will just echo back the data which it recieves.
5 """
6
7 import logging
8
9
10 log = logging.getLogger(__name__)
11
12
13 class EchoApp(object):
14
15 def __init__(self, repo_path, repo_name, config):
16 self._repo_path = repo_path
17 log.info("EchoApp initialized for %s", repo_path)
18
19 def __call__(self, environ, start_response):
20 log.debug("EchoApp called for %s", self._repo_path)
21 log.debug("Content-Length: %s", environ.get('CONTENT_LENGTH'))
22 environ['wsgi.input'].read()
23 status = '200 OK'
24 headers = []
25 start_response(status, headers)
26 return ["ECHO"]
27
28
29 def create_app():
30 """
31 Allows to run this app directly in a WSGI server.
32 """
33 stub_config = {}
34 return EchoApp('stub_path', 'stub_name', stub_config)
@@ -0,0 +1,45 b''
1 """
2 Provides the same API as :mod:`remote_wsgi`.
3
4 Uses the `EchoApp` instead of real implementations.
5 """
6
7 import logging
8
9 from .echo_app import EchoApp
10 from vcsserver import wsgi_app_caller
11
12
13 log = logging.getLogger(__name__)
14
15
16 class GitRemoteWsgi(object):
17 def handle(self, environ, input_data, *args, **kwargs):
18 app = wsgi_app_caller.WSGIAppCaller(
19 create_echo_wsgi_app(*args, **kwargs))
20
21 return app.handle(environ, input_data)
22
23
24 class HgRemoteWsgi(object):
25 def handle(self, environ, input_data, *args, **kwargs):
26 app = wsgi_app_caller.WSGIAppCaller(
27 create_echo_wsgi_app(*args, **kwargs))
28
29 return app.handle(environ, input_data)
30
31
32 def create_echo_wsgi_app(repo_path, repo_name, config):
33 log.debug("Creating EchoApp WSGI application")
34
35 _assert_valid_config(config)
36
37 # Remaining items are forwarded to have the extras available
38 return EchoApp(repo_path, repo_name, config=config)
39
40
41 def _assert_valid_config(config):
42 config = config.copy()
43
44 # This is what git needs from config at this stage
45 config.pop('git_update_server_info')
@@ -0,0 +1,56 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """
19 Special exception handling over the wire.
20
21 Since we cannot assume that our client is able to import our exception classes,
22 this module provides a "wrapping" mechanism to raise plain exceptions
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 different error conditions.
25 """
26
27 import functools
28
29
30 def _make_exception(kind, *args):
31 """
32 Prepares a base `Exception` instance to be sent over the wire.
33
34 To give our caller a hint what this is about, it will attach an attribute
35 `_vcs_kind` to the exception.
36 """
37 exc = Exception(*args)
38 exc._vcs_kind = kind
39 return exc
40
41
42 AbortException = functools.partial(_make_exception, 'abort')
43
44 ArchiveException = functools.partial(_make_exception, 'archive')
45
46 LookupException = functools.partial(_make_exception, 'lookup')
47
48 VcsException = functools.partial(_make_exception, 'error')
49
50 RepositoryLockedException = functools.partial(_make_exception, 'repo_locked')
51
52 RequirementException = functools.partial(_make_exception, 'requirement')
53
54 UnhandledException = functools.partial(_make_exception, 'unhandled')
55
56 URLError = functools.partial(_make_exception, 'url_error')
This diff has been collapsed as it changes many lines, (588 lines changed) Show them Hide them
@@ -0,0 +1,588 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import logging
19 import os
20 import posixpath as vcspath
21 import re
22 import stat
23 import urllib
24 import urllib2
25 from functools import wraps
26
27 from dulwich import index, objects
28 from dulwich.client import HttpGitClient, LocalGitClient
29 from dulwich.errors import (
30 NotGitRepository, ChecksumMismatch, WrongObjectException,
31 MissingCommitError, ObjectMissing, HangupException,
32 UnexpectedCommandError)
33 from dulwich.repo import Repo as DulwichRepo, Tag
34 from dulwich.server import update_server_info
35
36 from vcsserver import exceptions, settings, subprocessio
37 from vcsserver.utils import safe_str
38 from vcsserver.base import RepoFactory
39 from vcsserver.hgcompat import (
40 hg_url, httpbasicauthhandler, httpdigestauthhandler)
41
42
43 DIR_STAT = stat.S_IFDIR
44 FILE_MODE = stat.S_IFMT
45 GIT_LINK = objects.S_IFGITLINK
46
47 log = logging.getLogger(__name__)
48
49
50 def reraise_safe_exceptions(func):
51 """Converts Dulwich exceptions to something neutral."""
52 @wraps(func)
53 def wrapper(*args, **kwargs):
54 try:
55 return func(*args, **kwargs)
56 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
57 ObjectMissing) as e:
58 raise exceptions.LookupException(e.message)
59 except (HangupException, UnexpectedCommandError) as e:
60 raise exceptions.VcsException(e.message)
61 return wrapper
62
63
64 class Repo(DulwichRepo):
65 """
66 A wrapper for dulwich Repo class.
67
68 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
69 "Too many open files" error. We need to close all opened file descriptors
70 once the repo object is destroyed.
71
72 TODO: mikhail: please check if we need this wrapper after updating dulwich
73 to 0.12.0 +
74 """
75 def __del__(self):
76 if hasattr(self, 'object_store'):
77 self.close()
78
79
80 class GitFactory(RepoFactory):
81
82 def _create_repo(self, wire, create):
83 repo_path = str_to_dulwich(wire['path'])
84 return Repo(repo_path)
85
86
87 class GitRemote(object):
88
89 def __init__(self, factory):
90 self._factory = factory
91
92 self._bulk_methods = {
93 "author": self.commit_attribute,
94 "date": self.get_object_attrs,
95 "message": self.commit_attribute,
96 "parents": self.commit_attribute,
97 "_commit": self.revision,
98 }
99
100 def _assign_ref(self, wire, ref, commit_id):
101 repo = self._factory.repo(wire)
102 repo[ref] = commit_id
103
104 @reraise_safe_exceptions
105 def add_object(self, wire, content):
106 repo = self._factory.repo(wire)
107 blob = objects.Blob()
108 blob.set_raw_string(content)
109 repo.object_store.add_object(blob)
110 return blob.id
111
112 @reraise_safe_exceptions
113 def assert_correct_path(self, wire):
114 try:
115 self._factory.repo(wire)
116 except NotGitRepository as e:
117 # Exception can contain unicode which we convert
118 raise exceptions.AbortException(repr(e))
119
120 @reraise_safe_exceptions
121 def bare(self, wire):
122 repo = self._factory.repo(wire)
123 return repo.bare
124
125 @reraise_safe_exceptions
126 def blob_as_pretty_string(self, wire, sha):
127 repo = self._factory.repo(wire)
128 return repo[sha].as_pretty_string()
129
130 @reraise_safe_exceptions
131 def blob_raw_length(self, wire, sha):
132 repo = self._factory.repo(wire)
133 blob = repo[sha]
134 return blob.raw_length()
135
136 @reraise_safe_exceptions
137 def bulk_request(self, wire, rev, pre_load):
138 result = {}
139 for attr in pre_load:
140 try:
141 method = self._bulk_methods[attr]
142 args = [wire, rev]
143 if attr == "date":
144 args.extend(["commit_time", "commit_timezone"])
145 elif attr in ["author", "message", "parents"]:
146 args.append(attr)
147 result[attr] = method(*args)
148 except KeyError:
149 raise exceptions.VcsException(
150 "Unknown bulk attribute: %s" % attr)
151 return result
152
153 def _build_opener(self, url):
154 handlers = []
155 url_obj = hg_url(url)
156 _, authinfo = url_obj.authinfo()
157
158 if authinfo:
159 # create a password manager
160 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
161 passmgr.add_password(*authinfo)
162
163 handlers.extend((httpbasicauthhandler(passmgr),
164 httpdigestauthhandler(passmgr)))
165
166 return urllib2.build_opener(*handlers)
167
168 @reraise_safe_exceptions
169 def check_url(self, url, config):
170 url_obj = hg_url(url)
171 test_uri, _ = url_obj.authinfo()
172 url_obj.passwd = '*****'
173 cleaned_uri = str(url_obj)
174
175 if not test_uri.endswith('info/refs'):
176 test_uri = test_uri.rstrip('/') + '/info/refs'
177
178 o = self._build_opener(url)
179 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
180
181 q = {"service": 'git-upload-pack'}
182 qs = '?%s' % urllib.urlencode(q)
183 cu = "%s%s" % (test_uri, qs)
184 req = urllib2.Request(cu, None, {})
185
186 try:
187 resp = o.open(req)
188 if resp.code != 200:
189 raise Exception('Return Code is not 200')
190 except Exception as e:
191 # means it cannot be cloned
192 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
193
194 # now detect if it's proper git repo
195 gitdata = resp.read()
196 if 'service=git-upload-pack' in gitdata:
197 pass
198 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
199 # old style git can return some other format !
200 pass
201 else:
202 raise urllib2.URLError(
203 "url [%s] does not look like an git" % (cleaned_uri,))
204
205 return True
206
207 @reraise_safe_exceptions
208 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
209 remote_refs = self.fetch(wire, url, apply_refs=False)
210 repo = self._factory.repo(wire)
211 if isinstance(valid_refs, list):
212 valid_refs = tuple(valid_refs)
213
214 for k in remote_refs:
215 # only parse heads/tags and skip so called deferred tags
216 if k.startswith(valid_refs) and not k.endswith(deferred):
217 repo[k] = remote_refs[k]
218
219 if update_after_clone:
220 # we want to checkout HEAD
221 repo["HEAD"] = remote_refs["HEAD"]
222 index.build_index_from_tree(repo.path, repo.index_path(),
223 repo.object_store, repo["HEAD"].tree)
224
225 # TODO: this is quite complex, check if that can be simplified
226 @reraise_safe_exceptions
227 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
228 repo = self._factory.repo(wire)
229 object_store = repo.object_store
230
231 # Create tree and populates it with blobs
232 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
233
234 for node in updated:
235 # Compute subdirs if needed
236 dirpath, nodename = vcspath.split(node['path'])
237 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
238 parent = commit_tree
239 ancestors = [('', parent)]
240
241 # Tries to dig for the deepest existing tree
242 while dirnames:
243 curdir = dirnames.pop(0)
244 try:
245 dir_id = parent[curdir][1]
246 except KeyError:
247 # put curdir back into dirnames and stops
248 dirnames.insert(0, curdir)
249 break
250 else:
251 # If found, updates parent
252 parent = repo[dir_id]
253 ancestors.append((curdir, parent))
254 # Now parent is deepest existing tree and we need to create
255 # subtrees for dirnames (in reverse order)
256 # [this only applies for nodes from added]
257 new_trees = []
258
259 blob = objects.Blob.from_string(node['content'])
260
261 if dirnames:
262 # If there are trees which should be created we need to build
263 # them now (in reverse order)
264 reversed_dirnames = list(reversed(dirnames))
265 curtree = objects.Tree()
266 curtree[node['node_path']] = node['mode'], blob.id
267 new_trees.append(curtree)
268 for dirname in reversed_dirnames[:-1]:
269 newtree = objects.Tree()
270 newtree[dirname] = (DIR_STAT, curtree.id)
271 new_trees.append(newtree)
272 curtree = newtree
273 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
274 else:
275 parent.add(
276 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
277
278 new_trees.append(parent)
279 # Update ancestors
280 reversed_ancestors = reversed(
281 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
282 for parent, tree, path in reversed_ancestors:
283 parent[path] = (DIR_STAT, tree.id)
284 object_store.add_object(tree)
285
286 object_store.add_object(blob)
287 for tree in new_trees:
288 object_store.add_object(tree)
289
290 for node_path in removed:
291 paths = node_path.split('/')
292 tree = commit_tree
293 trees = [tree]
294 # Traverse deep into the forest...
295 for path in paths:
296 try:
297 obj = repo[tree[path][1]]
298 if isinstance(obj, objects.Tree):
299 trees.append(obj)
300 tree = obj
301 except KeyError:
302 break
303 # Cut down the blob and all rotten trees on the way back...
304 for path, tree in reversed(zip(paths, trees)):
305 del tree[path]
306 if tree:
307 # This tree still has elements - don't remove it or any
308 # of it's parents
309 break
310
311 object_store.add_object(commit_tree)
312
313 # Create commit
314 commit = objects.Commit()
315 commit.tree = commit_tree.id
316 for k, v in commit_data.iteritems():
317 setattr(commit, k, v)
318 object_store.add_object(commit)
319
320 ref = 'refs/heads/%s' % branch
321 repo.refs[ref] = commit.id
322
323 return commit.id
324
325 @reraise_safe_exceptions
326 def fetch(self, wire, url, apply_refs=True, refs=None):
327 if url != 'default' and '://' not in url:
328 client = LocalGitClient(url)
329 else:
330 url_obj = hg_url(url)
331 o = self._build_opener(url)
332 url, _ = url_obj.authinfo()
333 client = HttpGitClient(base_url=url, opener=o)
334 repo = self._factory.repo(wire)
335
336 determine_wants = repo.object_store.determine_wants_all
337 if refs:
338 def determine_wants_requested(references):
339 return [references[r] for r in references if r in refs]
340 determine_wants = determine_wants_requested
341
342 try:
343 remote_refs = client.fetch(
344 path=url, target=repo, determine_wants=determine_wants)
345 except NotGitRepository:
346 log.warning(
347 'Trying to fetch from "%s" failed, not a Git repository.', url)
348 raise exceptions.AbortException()
349
350 # mikhail: client.fetch() returns all the remote refs, but fetches only
351 # refs filtered by `determine_wants` function. We need to filter result
352 # as well
353 if refs:
354 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
355
356 if apply_refs:
357 # TODO: johbo: Needs proper test coverage with a git repository
358 # that contains a tag object, so that we would end up with
359 # a peeled ref at this point.
360 PEELED_REF_MARKER = '^{}'
361 for k in remote_refs:
362 if k.endswith(PEELED_REF_MARKER):
363 log.info("Skipping peeled reference %s", k)
364 continue
365 repo[k] = remote_refs[k]
366
367 if refs:
368 # mikhail: explicitly set the head to the last ref.
369 repo['HEAD'] = remote_refs[refs[-1]]
370
371 # TODO: mikhail: should we return remote_refs here to be
372 # consistent?
373 else:
374 return remote_refs
375
376 @reraise_safe_exceptions
377 def get_remote_refs(self, wire, url):
378 repo = Repo(url)
379 return repo.get_refs()
380
381 @reraise_safe_exceptions
382 def get_description(self, wire):
383 repo = self._factory.repo(wire)
384 return repo.get_description()
385
386 @reraise_safe_exceptions
387 def get_file_history(self, wire, file_path, commit_id, limit):
388 repo = self._factory.repo(wire)
389 include = [commit_id]
390 paths = [file_path]
391
392 walker = repo.get_walker(include, paths=paths, max_entries=limit)
393 return [x.commit.id for x in walker]
394
395 @reraise_safe_exceptions
396 def get_missing_revs(self, wire, rev1, rev2, path2):
397 repo = self._factory.repo(wire)
398 LocalGitClient(thin_packs=False).fetch(path2, repo)
399
400 wire_remote = wire.copy()
401 wire_remote['path'] = path2
402 repo_remote = self._factory.repo(wire_remote)
403 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
404
405 revs = [
406 x.commit.id
407 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
408 return revs
409
410 @reraise_safe_exceptions
411 def get_object(self, wire, sha):
412 repo = self._factory.repo(wire)
413 obj = repo.get_object(sha)
414 commit_id = obj.id
415
416 if isinstance(obj, Tag):
417 commit_id = obj.object[1]
418
419 return {
420 'id': obj.id,
421 'type': obj.type_name,
422 'commit_id': commit_id
423 }
424
425 @reraise_safe_exceptions
426 def get_object_attrs(self, wire, sha, *attrs):
427 repo = self._factory.repo(wire)
428 obj = repo.get_object(sha)
429 return list(getattr(obj, a) for a in attrs)
430
431 @reraise_safe_exceptions
432 def get_refs(self, wire, keys=None):
433 # FIXME(skreft): this method is affected by bug
434 # http://bugs.rhodecode.com/issues/298.
435 # Basically, it will overwrite previously computed references if
436 # there's another one with the same name and given the order of
437 # repo.get_refs() is not guaranteed, the output of this method is not
438 # stable either.
439 repo = self._factory.repo(wire)
440 refs = repo.get_refs()
441 if keys is None:
442 return refs
443
444 _refs = {}
445 for ref, sha in refs.iteritems():
446 for k, type_ in keys:
447 if ref.startswith(k):
448 _key = ref[len(k):]
449 if type_ == 'T':
450 sha = repo.get_object(sha).id
451 _refs[_key] = [sha, type_]
452 break
453 return _refs
454
455 @reraise_safe_exceptions
456 def get_refs_path(self, wire):
457 repo = self._factory.repo(wire)
458 return repo.refs.path
459
460 @reraise_safe_exceptions
461 def head(self, wire):
462 repo = self._factory.repo(wire)
463 return repo.head()
464
465 @reraise_safe_exceptions
466 def init(self, wire):
467 repo_path = str_to_dulwich(wire['path'])
468 self.repo = Repo.init(repo_path)
469
470 @reraise_safe_exceptions
471 def init_bare(self, wire):
472 repo_path = str_to_dulwich(wire['path'])
473 self.repo = Repo.init_bare(repo_path)
474
475 @reraise_safe_exceptions
476 def revision(self, wire, rev):
477 repo = self._factory.repo(wire)
478 obj = repo[rev]
479 obj_data = {
480 'id': obj.id,
481 }
482 try:
483 obj_data['tree'] = obj.tree
484 except AttributeError:
485 pass
486 return obj_data
487
488 @reraise_safe_exceptions
489 def commit_attribute(self, wire, rev, attr):
490 repo = self._factory.repo(wire)
491 obj = repo[rev]
492 return getattr(obj, attr)
493
494 @reraise_safe_exceptions
495 def set_refs(self, wire, key, value):
496 repo = self._factory.repo(wire)
497 repo.refs[key] = value
498
499 @reraise_safe_exceptions
500 def remove_ref(self, wire, key):
501 repo = self._factory.repo(wire)
502 del repo.refs[key]
503
504 @reraise_safe_exceptions
505 def tree_changes(self, wire, source_id, target_id):
506 repo = self._factory.repo(wire)
507 source = repo[source_id].tree if source_id else None
508 target = repo[target_id].tree
509 result = repo.object_store.tree_changes(source, target)
510 return list(result)
511
512 @reraise_safe_exceptions
513 def tree_items(self, wire, tree_id):
514 repo = self._factory.repo(wire)
515 tree = repo[tree_id]
516
517 result = []
518 for item in tree.iteritems():
519 item_sha = item.sha
520 item_mode = item.mode
521
522 if FILE_MODE(item_mode) == GIT_LINK:
523 item_type = "link"
524 else:
525 item_type = repo[item_sha].type_name
526
527 result.append((item.path, item_mode, item_sha, item_type))
528 return result
529
530 @reraise_safe_exceptions
531 def update_server_info(self, wire):
532 repo = self._factory.repo(wire)
533 update_server_info(repo)
534
535 @reraise_safe_exceptions
536 def discover_git_version(self):
537 stdout, _ = self.run_git_command(
538 {}, ['--version'], _bare=True, _safe=True)
539 return stdout
540
541 @reraise_safe_exceptions
542 def run_git_command(self, wire, cmd, **opts):
543 path = wire.get('path', None)
544
545 if path and os.path.isdir(path):
546 opts['cwd'] = path
547
548 if '_bare' in opts:
549 _copts = []
550 del opts['_bare']
551 else:
552 _copts = ['-c', 'core.quotepath=false', ]
553 safe_call = False
554 if '_safe' in opts:
555 # no exc on failure
556 del opts['_safe']
557 safe_call = True
558
559 gitenv = os.environ.copy()
560 gitenv.update(opts.pop('extra_env', {}))
561 # need to clean fix GIT_DIR !
562 if 'GIT_DIR' in gitenv:
563 del gitenv['GIT_DIR']
564 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
565
566 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
567
568 try:
569 _opts = {'env': gitenv, 'shell': False}
570 _opts.update(opts)
571 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
572
573 return ''.join(p), ''.join(p.error)
574 except (EnvironmentError, OSError) as err:
575 tb_err = ("Couldn't run git command (%s).\n"
576 "Original error was:%s\n" % (cmd, err))
577 log.exception(tb_err)
578 if safe_call:
579 return '', err
580 else:
581 raise exceptions.VcsException(tb_err)
582
583
584 def str_to_dulwich(value):
585 """
586 Dulwich 0.10.1a requires `unicode` objects to be passed in.
587 """
588 return value.decode(settings.WIRE_ENCODING)
This diff has been collapsed as it changes many lines, (692 lines changed) Show them Hide them
@@ -0,0 +1,692 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import io
19 import logging
20 import stat
21 import sys
22 import urllib
23 import urllib2
24
25 from hgext import largefiles, rebase
26 from hgext.strip import strip as hgext_strip
27 from mercurial import commands
28 from mercurial import unionrepo
29
30 from vcsserver import exceptions
31 from vcsserver.base import RepoFactory
32 from vcsserver.hgcompat import (
33 archival, bin, clone, config as hgconfig, diffopts, hex, hg_url,
34 httpbasicauthhandler, httpdigestauthhandler, httppeer, localrepository,
35 match, memctx, exchange, memfilectx, nullrev, patch, peer, revrange, ui,
36 Abort, LookupError, RepoError, RepoLookupError, InterventionRequired,
37 RequirementError)
38
39 log = logging.getLogger(__name__)
40
41
42 def make_ui_from_config(repo_config):
43 baseui = ui.ui()
44
45 # clean the baseui object
46 baseui._ocfg = hgconfig.config()
47 baseui._ucfg = hgconfig.config()
48 baseui._tcfg = hgconfig.config()
49
50 for section, option, value in repo_config:
51 baseui.setconfig(section, option, value)
52
53 # make our hgweb quiet so it doesn't print output
54 baseui.setconfig('ui', 'quiet', 'true')
55
56 # force mercurial to only use 1 thread, otherwise it may try to set a
57 # signal in a non-main thread, thus generating a ValueError.
58 baseui.setconfig('worker', 'numcpus', 1)
59
60 return baseui
61
62
63 def reraise_safe_exceptions(func):
64 """Decorator for converting mercurial exceptions to something neutral."""
65 def wrapper(*args, **kwargs):
66 try:
67 return func(*args, **kwargs)
68 except (Abort, InterventionRequired):
69 raise_from_original(exceptions.AbortException)
70 except RepoLookupError:
71 raise_from_original(exceptions.LookupException)
72 except RequirementError:
73 raise_from_original(exceptions.RequirementException)
74 except RepoError:
75 raise_from_original(exceptions.VcsException)
76 except LookupError:
77 raise_from_original(exceptions.LookupException)
78 except Exception as e:
79 if not hasattr(e, '_vcs_kind'):
80 log.exception("Unhandled exception in hg remote call")
81 raise_from_original(exceptions.UnhandledException)
82 raise
83 return wrapper
84
85
86 def raise_from_original(new_type):
87 """
88 Raise a new exception type with original args and traceback.
89 """
90 _, original, traceback = sys.exc_info()
91 try:
92 raise new_type(*original.args), None, traceback
93 finally:
94 del traceback
95
96
97 class MercurialFactory(RepoFactory):
98
99 def _create_config(self, config, hooks=True):
100 if not hooks:
101 hooks_to_clean = frozenset((
102 'changegroup.repo_size', 'preoutgoing.pre_pull',
103 'outgoing.pull_logger', 'prechangegroup.pre_push'))
104 new_config = []
105 for section, option, value in config:
106 if section == 'hooks' and option in hooks_to_clean:
107 continue
108 new_config.append((section, option, value))
109 config = new_config
110
111 baseui = make_ui_from_config(config)
112 return baseui
113
114 def _create_repo(self, wire, create):
115 baseui = self._create_config(wire["config"])
116 return localrepository(baseui, wire["path"], create)
117
118
119 class HgRemote(object):
120
121 def __init__(self, factory):
122 self._factory = factory
123
124 self._bulk_methods = {
125 "affected_files": self.ctx_files,
126 "author": self.ctx_user,
127 "branch": self.ctx_branch,
128 "children": self.ctx_children,
129 "date": self.ctx_date,
130 "message": self.ctx_description,
131 "parents": self.ctx_parents,
132 "status": self.ctx_status,
133 "_file_paths": self.ctx_list,
134 }
135
136 @reraise_safe_exceptions
137 def archive_repo(self, archive_path, mtime, file_info, kind):
138 if kind == "tgz":
139 archiver = archival.tarit(archive_path, mtime, "gz")
140 elif kind == "tbz2":
141 archiver = archival.tarit(archive_path, mtime, "bz2")
142 elif kind == 'zip':
143 archiver = archival.zipit(archive_path, mtime)
144 else:
145 raise exceptions.ArchiveException(
146 'Remote does not support: "%s".' % kind)
147
148 for f_path, f_mode, f_is_link, f_content in file_info:
149 archiver.addfile(f_path, f_mode, f_is_link, f_content)
150 archiver.done()
151
152 @reraise_safe_exceptions
153 def bookmarks(self, wire):
154 repo = self._factory.repo(wire)
155 return dict(repo._bookmarks)
156
157 @reraise_safe_exceptions
158 def branches(self, wire, normal, closed):
159 repo = self._factory.repo(wire)
160 iter_branches = repo.branchmap().iterbranches()
161 bt = {}
162 for branch_name, _heads, tip, is_closed in iter_branches:
163 if normal and not is_closed:
164 bt[branch_name] = tip
165 if closed and is_closed:
166 bt[branch_name] = tip
167
168 return bt
169
170 @reraise_safe_exceptions
171 def bulk_request(self, wire, rev, pre_load):
172 result = {}
173 for attr in pre_load:
174 try:
175 method = self._bulk_methods[attr]
176 result[attr] = method(wire, rev)
177 except KeyError:
178 raise exceptions.VcsException(
179 'Unknown bulk attribute: "%s"' % attr)
180 return result
181
182 @reraise_safe_exceptions
183 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
184 baseui = self._factory._create_config(wire["config"], hooks=hooks)
185 clone(baseui, source, dest, noupdate=not update_after_clone)
186
187 @reraise_safe_exceptions
188 def commitctx(
189 self, wire, message, parents, commit_time, commit_timezone,
190 user, files, extra, removed, updated):
191
192 def _filectxfn(_repo, memctx, path):
193 """
194 Marks given path as added/changed/removed in a given _repo. This is
195 for internal mercurial commit function.
196 """
197
198 # check if this path is removed
199 if path in removed:
200 # returning None is a way to mark node for removal
201 return None
202
203 # check if this path is added
204 for node in updated:
205 if node['path'] == path:
206 return memfilectx(
207 _repo,
208 path=node['path'],
209 data=node['content'],
210 islink=False,
211 isexec=bool(node['mode'] & stat.S_IXUSR),
212 copied=False,
213 memctx=memctx)
214
215 raise exceptions.AbortException(
216 "Given path haven't been marked as added, "
217 "changed or removed (%s)" % path)
218
219 repo = self._factory.repo(wire)
220
221 commit_ctx = memctx(
222 repo=repo,
223 parents=parents,
224 text=message,
225 files=files,
226 filectxfn=_filectxfn,
227 user=user,
228 date=(commit_time, commit_timezone),
229 extra=extra)
230
231 n = repo.commitctx(commit_ctx)
232 new_id = hex(n)
233
234 return new_id
235
236 @reraise_safe_exceptions
237 def ctx_branch(self, wire, revision):
238 repo = self._factory.repo(wire)
239 ctx = repo[revision]
240 return ctx.branch()
241
242 @reraise_safe_exceptions
243 def ctx_children(self, wire, revision):
244 repo = self._factory.repo(wire)
245 ctx = repo[revision]
246 return [child.rev() for child in ctx.children()]
247
248 @reraise_safe_exceptions
249 def ctx_date(self, wire, revision):
250 repo = self._factory.repo(wire)
251 ctx = repo[revision]
252 return ctx.date()
253
254 @reraise_safe_exceptions
255 def ctx_description(self, wire, revision):
256 repo = self._factory.repo(wire)
257 ctx = repo[revision]
258 return ctx.description()
259
260 @reraise_safe_exceptions
261 def ctx_diff(
262 self, wire, revision, git=True, ignore_whitespace=True, context=3):
263 repo = self._factory.repo(wire)
264 ctx = repo[revision]
265 result = ctx.diff(
266 git=git, ignore_whitespace=ignore_whitespace, context=context)
267 return list(result)
268
269 @reraise_safe_exceptions
270 def ctx_files(self, wire, revision):
271 repo = self._factory.repo(wire)
272 ctx = repo[revision]
273 return ctx.files()
274
275 @reraise_safe_exceptions
276 def ctx_list(self, path, revision):
277 repo = self._factory.repo(path)
278 ctx = repo[revision]
279 return list(ctx)
280
281 @reraise_safe_exceptions
282 def ctx_parents(self, wire, revision):
283 repo = self._factory.repo(wire)
284 ctx = repo[revision]
285 return [parent.rev() for parent in ctx.parents()]
286
287 @reraise_safe_exceptions
288 def ctx_substate(self, wire, revision):
289 repo = self._factory.repo(wire)
290 ctx = repo[revision]
291 return ctx.substate
292
293 @reraise_safe_exceptions
294 def ctx_status(self, wire, revision):
295 repo = self._factory.repo(wire)
296 ctx = repo[revision]
297 status = repo[ctx.p1().node()].status(other=ctx.node())
298 # object of status (odd, custom named tuple in mercurial) is not
299 # correctly serializable via Pyro, we make it a list, as the underling
300 # API expects this to be a list
301 return list(status)
302
303 @reraise_safe_exceptions
304 def ctx_user(self, wire, revision):
305 repo = self._factory.repo(wire)
306 ctx = repo[revision]
307 return ctx.user()
308
309 @reraise_safe_exceptions
310 def check_url(self, url, config):
311 _proto = None
312 if '+' in url[:url.find('://')]:
313 _proto = url[0:url.find('+')]
314 url = url[url.find('+') + 1:]
315 handlers = []
316 url_obj = hg_url(url)
317 test_uri, authinfo = url_obj.authinfo()
318 url_obj.passwd = '*****'
319 cleaned_uri = str(url_obj)
320
321 if authinfo:
322 # create a password manager
323 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
324 passmgr.add_password(*authinfo)
325
326 handlers.extend((httpbasicauthhandler(passmgr),
327 httpdigestauthhandler(passmgr)))
328
329 o = urllib2.build_opener(*handlers)
330 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
331 ('Accept', 'application/mercurial-0.1')]
332
333 q = {"cmd": 'between'}
334 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
335 qs = '?%s' % urllib.urlencode(q)
336 cu = "%s%s" % (test_uri, qs)
337 req = urllib2.Request(cu, None, {})
338
339 try:
340 resp = o.open(req)
341 if resp.code != 200:
342 raise exceptions.URLError('Return Code is not 200')
343 except Exception as e:
344 # means it cannot be cloned
345 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
346
347 # now check if it's a proper hg repo, but don't do it for svn
348 try:
349 if _proto == 'svn':
350 pass
351 else:
352 # check for pure hg repos
353 httppeer(make_ui_from_config(config), url).lookup('tip')
354 except Exception as e:
355 raise exceptions.URLError(
356 "url [%s] does not look like an hg repo org_exc: %s"
357 % (cleaned_uri, e))
358
359 return True
360
361 @reraise_safe_exceptions
362 def diff(
363 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
364 context):
365 repo = self._factory.repo(wire)
366
367 if file_filter:
368 filter = match(file_filter[0], '', [file_filter[1]])
369 else:
370 filter = file_filter
371 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
372
373 try:
374 return "".join(patch.diff(
375 repo, node1=rev1, node2=rev2, match=filter, opts=opts))
376 except RepoLookupError:
377 raise exceptions.LookupException()
378
379 @reraise_safe_exceptions
380 def file_history(self, wire, revision, path, limit):
381 repo = self._factory.repo(wire)
382
383 ctx = repo[revision]
384 fctx = ctx.filectx(path)
385
386 def history_iter():
387 limit_rev = fctx.rev()
388 for obj in reversed(list(fctx.filelog())):
389 obj = fctx.filectx(obj)
390 if limit_rev >= obj.rev():
391 yield obj
392
393 history = []
394 for cnt, obj in enumerate(history_iter()):
395 if limit and cnt >= limit:
396 break
397 history.append(hex(obj.node()))
398
399 return [x for x in history]
400
401 @reraise_safe_exceptions
402 def file_history_untill(self, wire, revision, path, limit):
403 repo = self._factory.repo(wire)
404 ctx = repo[revision]
405 fctx = ctx.filectx(path)
406
407 file_log = list(fctx.filelog())
408 if limit:
409 # Limit to the last n items
410 file_log = file_log[-limit:]
411
412 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
413
414 @reraise_safe_exceptions
415 def fctx_annotate(self, wire, revision, path):
416 repo = self._factory.repo(wire)
417 ctx = repo[revision]
418 fctx = ctx.filectx(path)
419
420 result = []
421 for i, annotate_data in enumerate(fctx.annotate()):
422 ln_no = i + 1
423 sha = hex(annotate_data[0].node())
424 result.append((ln_no, sha, annotate_data[1]))
425 return result
426
427 @reraise_safe_exceptions
428 def fctx_data(self, wire, revision, path):
429 repo = self._factory.repo(wire)
430 ctx = repo[revision]
431 fctx = ctx.filectx(path)
432 return fctx.data()
433
434 @reraise_safe_exceptions
435 def fctx_flags(self, wire, revision, path):
436 repo = self._factory.repo(wire)
437 ctx = repo[revision]
438 fctx = ctx.filectx(path)
439 return fctx.flags()
440
441 @reraise_safe_exceptions
442 def fctx_size(self, wire, revision, path):
443 repo = self._factory.repo(wire)
444 ctx = repo[revision]
445 fctx = ctx.filectx(path)
446 return fctx.size()
447
448 @reraise_safe_exceptions
449 def get_all_commit_ids(self, wire, name):
450 repo = self._factory.repo(wire)
451 revs = repo.filtered(name).changelog.index
452 return map(lambda x: hex(x[7]), revs)[:-1]
453
454 @reraise_safe_exceptions
455 def get_config_value(self, wire, section, name, untrusted=False):
456 repo = self._factory.repo(wire)
457 return repo.ui.config(section, name, untrusted=untrusted)
458
459 @reraise_safe_exceptions
460 def get_config_bool(self, wire, section, name, untrusted=False):
461 repo = self._factory.repo(wire)
462 return repo.ui.configbool(section, name, untrusted=untrusted)
463
464 @reraise_safe_exceptions
465 def get_config_list(self, wire, section, name, untrusted=False):
466 repo = self._factory.repo(wire)
467 return repo.ui.configlist(section, name, untrusted=untrusted)
468
469 @reraise_safe_exceptions
470 def is_large_file(self, wire, path):
471 return largefiles.lfutil.isstandin(path)
472
473 @reraise_safe_exceptions
474 def in_store(self, wire, sha):
475 repo = self._factory.repo(wire)
476 return largefiles.lfutil.instore(repo, sha)
477
478 @reraise_safe_exceptions
479 def in_user_cache(self, wire, sha):
480 repo = self._factory.repo(wire)
481 return largefiles.lfutil.inusercache(repo.ui, sha)
482
483 @reraise_safe_exceptions
484 def store_path(self, wire, sha):
485 repo = self._factory.repo(wire)
486 return largefiles.lfutil.storepath(repo, sha)
487
488 @reraise_safe_exceptions
489 def link(self, wire, sha, path):
490 repo = self._factory.repo(wire)
491 largefiles.lfutil.link(
492 largefiles.lfutil.usercachepath(repo.ui, sha), path)
493
494 @reraise_safe_exceptions
495 def localrepository(self, wire, create=False):
496 self._factory.repo(wire, create=create)
497
498 @reraise_safe_exceptions
499 def lookup(self, wire, revision, both):
500 # TODO Paris: Ugly hack to "deserialize" long for msgpack
501 if isinstance(revision, float):
502 revision = long(revision)
503 repo = self._factory.repo(wire)
504 try:
505 ctx = repo[revision]
506 except RepoLookupError:
507 raise exceptions.LookupException(revision)
508 except LookupError as e:
509 raise exceptions.LookupException(e.name)
510
511 if not both:
512 return ctx.hex()
513
514 ctx = repo[ctx.hex()]
515 return ctx.hex(), ctx.rev()
516
517 @reraise_safe_exceptions
518 def pull(self, wire, url, commit_ids=None):
519 repo = self._factory.repo(wire)
520 remote = peer(repo, {}, url)
521 if commit_ids:
522 commit_ids = [bin(commit_id) for commit_id in commit_ids]
523
524 return exchange.pull(
525 repo, remote, heads=commit_ids, force=None).cgresult
526
527 @reraise_safe_exceptions
528 def revision(self, wire, rev):
529 repo = self._factory.repo(wire)
530 ctx = repo[rev]
531 return ctx.rev()
532
533 @reraise_safe_exceptions
534 def rev_range(self, wire, filter):
535 repo = self._factory.repo(wire)
536 revisions = [rev for rev in revrange(repo, filter)]
537 return revisions
538
539 @reraise_safe_exceptions
540 def rev_range_hash(self, wire, node):
541 repo = self._factory.repo(wire)
542
543 def get_revs(repo, rev_opt):
544 if rev_opt:
545 revs = revrange(repo, rev_opt)
546 if len(revs) == 0:
547 return (nullrev, nullrev)
548 return max(revs), min(revs)
549 else:
550 return len(repo) - 1, 0
551
552 stop, start = get_revs(repo, [node + ':'])
553 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
554 return revs
555
556 @reraise_safe_exceptions
557 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
558 other_path = kwargs.pop('other_path', None)
559
560 # case when we want to compare two independent repositories
561 if other_path and other_path != wire["path"]:
562 baseui = self._factory._create_config(wire["config"])
563 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
564 else:
565 repo = self._factory.repo(wire)
566 return list(repo.revs(rev_spec, *args))
567
568 @reraise_safe_exceptions
569 def strip(self, wire, revision, update, backup):
570 repo = self._factory.repo(wire)
571 ctx = repo[revision]
572 hgext_strip(
573 repo.baseui, repo, ctx.node(), update=update, backup=backup)
574
575 @reraise_safe_exceptions
576 def tag(self, wire, name, revision, message, local, user,
577 tag_time, tag_timezone):
578 repo = self._factory.repo(wire)
579 ctx = repo[revision]
580 node = ctx.node()
581
582 date = (tag_time, tag_timezone)
583 try:
584 repo.tag(name, node, message, local, user, date)
585 except Abort:
586 log.exception("Tag operation aborted")
587 raise exceptions.AbortException()
588
589 @reraise_safe_exceptions
590 def tags(self, wire):
591 repo = self._factory.repo(wire)
592 return repo.tags()
593
594 @reraise_safe_exceptions
595 def update(self, wire, node=None, clean=False):
596 repo = self._factory.repo(wire)
597 baseui = self._factory._create_config(wire['config'])
598 commands.update(baseui, repo, node=node, clean=clean)
599
600 @reraise_safe_exceptions
601 def identify(self, wire):
602 repo = self._factory.repo(wire)
603 baseui = self._factory._create_config(wire['config'])
604 output = io.BytesIO()
605 baseui.write = output.write
606 # This is required to get a full node id
607 baseui.debugflag = True
608 commands.identify(baseui, repo, id=True)
609
610 return output.getvalue()
611
612 @reraise_safe_exceptions
613 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
614 hooks=True):
615 repo = self._factory.repo(wire)
616 baseui = self._factory._create_config(wire['config'], hooks=hooks)
617
618 # Mercurial internally has a lot of logic that checks ONLY if
619 # option is defined, we just pass those if they are defined then
620 opts = {}
621 if bookmark:
622 opts['bookmark'] = bookmark
623 if branch:
624 opts['branch'] = branch
625 if revision:
626 opts['rev'] = revision
627
628 commands.pull(baseui, repo, source, **opts)
629
630 @reraise_safe_exceptions
631 def heads(self, wire, branch=None):
632 repo = self._factory.repo(wire)
633 baseui = self._factory._create_config(wire['config'])
634 output = io.BytesIO()
635
636 def write(data, **unused_kwargs):
637 output.write(data)
638
639 baseui.write = write
640 if branch:
641 args = [branch]
642 else:
643 args = []
644 commands.heads(baseui, repo, template='{node} ', *args)
645
646 return output.getvalue()
647
648 @reraise_safe_exceptions
649 def ancestor(self, wire, revision1, revision2):
650 repo = self._factory.repo(wire)
651 baseui = self._factory._create_config(wire['config'])
652 output = io.BytesIO()
653 baseui.write = output.write
654 commands.debugancestor(baseui, repo, revision1, revision2)
655
656 return output.getvalue()
657
658 @reraise_safe_exceptions
659 def push(self, wire, revisions, dest_path, hooks=True,
660 push_branches=False):
661 repo = self._factory.repo(wire)
662 baseui = self._factory._create_config(wire['config'], hooks=hooks)
663 commands.push(baseui, repo, dest=dest_path, rev=revisions,
664 new_branch=push_branches)
665
666 @reraise_safe_exceptions
667 def merge(self, wire, revision):
668 repo = self._factory.repo(wire)
669 baseui = self._factory._create_config(wire['config'])
670 repo.ui.setconfig('ui', 'merge', 'internal:dump')
671 commands.merge(baseui, repo, rev=revision)
672
673 @reraise_safe_exceptions
674 def commit(self, wire, message, username):
675 repo = self._factory.repo(wire)
676 baseui = self._factory._create_config(wire['config'])
677 repo.ui.setconfig('ui', 'username', username)
678 commands.commit(baseui, repo, message=message)
679
680 @reraise_safe_exceptions
681 def rebase(self, wire, source=None, dest=None, abort=False):
682 repo = self._factory.repo(wire)
683 baseui = self._factory._create_config(wire['config'])
684 repo.ui.setconfig('ui', 'merge', 'internal:dump')
685 rebase.rebase(
686 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
687
688 @reraise_safe_exceptions
689 def bookmark(self, wire, bookmark, revision=None):
690 repo = self._factory.repo(wire)
691 baseui = self._factory._create_config(wire['config'])
692 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
@@ -0,0 +1,61 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """
19 Mercurial libs compatibility
20 """
21
22 import mercurial
23 import mercurial.demandimport
24 # patch demandimport, due to bug in mercurial when it always triggers
25 # demandimport.enable()
26 mercurial.demandimport.enable = lambda *args, **kwargs: 1
27
28 from mercurial import ui
29 from mercurial import patch
30 from mercurial import config
31 from mercurial import extensions
32 from mercurial import scmutil
33 from mercurial import archival
34 from mercurial import discovery
35 from mercurial import unionrepo
36 from mercurial import localrepo
37 from mercurial import merge as hg_merge
38
39 from mercurial.commands import clone, nullid, pull
40 from mercurial.context import memctx, memfilectx
41 from mercurial.error import (
42 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
43 RequirementError)
44 from mercurial.hgweb import hgweb_mod
45 from mercurial.localrepo import localrepository
46 from mercurial.match import match
47 from mercurial.mdiff import diffopts
48 from mercurial.node import bin, hex
49 from mercurial.encoding import tolocal
50 from mercurial.discovery import findcommonoutgoing
51 from mercurial.hg import peer
52 from mercurial.httppeer import httppeer
53 from mercurial.util import url as hg_url
54 from mercurial.scmutil import revrange
55 from mercurial.node import nullrev
56 from mercurial import exchange
57 from hgext import largefiles
58
59 # those authnadlers are patched for python 2.6.5 bug an
60 # infinit looping when given invalid resources
61 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
@@ -0,0 +1,60 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """
19 Adjustments to Mercurial
20
21 Intentionally kept separate from `hgcompat` and `hg`, so that these patches can
22 be applied without having to import the whole Mercurial machinery.
23
24 Imports are function local, so that just importing this module does not cause
25 side-effects other than these functions being defined.
26 """
27
28 import logging
29
30
31 def patch_largefiles_capabilities():
32 """
33 Patches the capabilities function in the largefiles extension.
34 """
35 from vcsserver import hgcompat
36 lfproto = hgcompat.largefiles.proto
37 wrapper = _dynamic_capabilities_wrapper(
38 lfproto, hgcompat.extensions.extensions)
39 lfproto.capabilities = wrapper
40
41
42 def _dynamic_capabilities_wrapper(lfproto, extensions):
43
44 wrapped_capabilities = lfproto.capabilities
45 logger = logging.getLogger('vcsserver.hg')
46
47 def _dynamic_capabilities(repo, proto):
48 """
49 Adds dynamic behavior, so that the capability is only added if the
50 extension is enabled in the current ui object.
51 """
52 if 'largefiles' in dict(extensions(repo.ui)):
53 logger.debug('Extension largefiles enabled')
54 calc_capabilities = wrapped_capabilities
55 else:
56 logger.debug('Extension largefiles disabled')
57 calc_capabilities = lfproto.capabilitiesorig
58 return calc_capabilities(repo, proto)
59
60 return _dynamic_capabilities
@@ -0,0 +1,372 b''
1 # -*- coding: utf-8 -*-
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2016 RodeCode GmbH
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20 import collections
21 import importlib
22 import io
23 import json
24 import subprocess
25 import sys
26 from httplib import HTTPConnection
27
28
29 import mercurial.scmutil
30 import mercurial.node
31 import Pyro4
32 import simplejson as json
33
34 from vcsserver import exceptions
35
36
37 class HooksHttpClient(object):
38 connection = None
39
40 def __init__(self, hooks_uri):
41 self.hooks_uri = hooks_uri
42
43 def __call__(self, method, extras):
44 connection = HTTPConnection(self.hooks_uri)
45 body = self._serialize(method, extras)
46 connection.request('POST', '/', body)
47 response = connection.getresponse()
48 return json.loads(response.read())
49
50 def _serialize(self, hook_name, extras):
51 data = {
52 'method': hook_name,
53 'extras': extras
54 }
55 return json.dumps(data)
56
57
58 class HooksDummyClient(object):
59 def __init__(self, hooks_module):
60 self._hooks_module = importlib.import_module(hooks_module)
61
62 def __call__(self, hook_name, extras):
63 with self._hooks_module.Hooks() as hooks:
64 return getattr(hooks, hook_name)(extras)
65
66
67 class HooksPyro4Client(object):
68 def __init__(self, hooks_uri):
69 self.hooks_uri = hooks_uri
70
71 def __call__(self, hook_name, extras):
72 with Pyro4.Proxy(self.hooks_uri) as hooks:
73 return getattr(hooks, hook_name)(extras)
74
75
76 class RemoteMessageWriter(object):
77 """Writer base class."""
78 def write(message):
79 raise NotImplementedError()
80
81
82 class HgMessageWriter(RemoteMessageWriter):
83 """Writer that knows how to send messages to mercurial clients."""
84
85 def __init__(self, ui):
86 self.ui = ui
87
88 def write(self, message):
89 # TODO: Check why the quiet flag is set by default.
90 old = self.ui.quiet
91 self.ui.quiet = False
92 self.ui.status(message.encode('utf-8'))
93 self.ui.quiet = old
94
95
96 class GitMessageWriter(RemoteMessageWriter):
97 """Writer that knows how to send messages to git clients."""
98
99 def __init__(self, stdout=None):
100 self.stdout = stdout or sys.stdout
101
102 def write(self, message):
103 self.stdout.write(message.encode('utf-8'))
104
105
106 def _handle_exception(result):
107 exception_class = result.get('exception')
108 if exception_class == 'HTTPLockedRC':
109 raise exceptions.RepositoryLockedException(*result['exception_args'])
110 elif exception_class == 'RepositoryError':
111 raise exceptions.VcsException(*result['exception_args'])
112 elif exception_class:
113 raise Exception('Got remote exception "%s" with args "%s"' %
114 (exception_class, result['exception_args']))
115
116
117 def _get_hooks_client(extras):
118 if 'hooks_uri' in extras:
119 protocol = extras.get('hooks_protocol')
120 return (
121 HooksHttpClient(extras['hooks_uri'])
122 if protocol == 'http'
123 else HooksPyro4Client(extras['hooks_uri'])
124 )
125 else:
126 return HooksDummyClient(extras['hooks_module'])
127
128
129 def _call_hook(hook_name, extras, writer):
130 hooks = _get_hooks_client(extras)
131 result = hooks(hook_name, extras)
132 writer.write(result['output'])
133 _handle_exception(result)
134
135 return result['status']
136
137
138 def _extras_from_ui(ui):
139 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
140 return extras
141
142
143 def repo_size(ui, repo, **kwargs):
144 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
145
146
147 def pre_pull(ui, repo, **kwargs):
148 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
149
150
151 def post_pull(ui, repo, **kwargs):
152 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
153
154
155 def pre_push(ui, repo, **kwargs):
156 return _call_hook('pre_push', _extras_from_ui(ui), HgMessageWriter(ui))
157
158
159 # N.B.(skreft): the two functions below were taken and adapted from
160 # rhodecode.lib.vcs.remote.handle_git_pre_receive
161 # They are required to compute the commit_ids
162 def _get_revs(repo, rev_opt):
163 revs = [rev for rev in mercurial.scmutil.revrange(repo, rev_opt)]
164 if len(revs) == 0:
165 return (mercurial.node.nullrev, mercurial.node.nullrev)
166
167 return max(revs), min(revs)
168
169
170 def _rev_range_hash(repo, node):
171 stop, start = _get_revs(repo, [node + ':'])
172 revs = [mercurial.node.hex(repo[r].node()) for r in xrange(start, stop + 1)]
173
174 return revs
175
176
177 def post_push(ui, repo, node, **kwargs):
178 commit_ids = _rev_range_hash(repo, node)
179
180 extras = _extras_from_ui(ui)
181 extras['commit_ids'] = commit_ids
182
183 return _call_hook('post_push', extras, HgMessageWriter(ui))
184
185
186 # backward compat
187 log_pull_action = post_pull
188
189 # backward compat
190 log_push_action = post_push
191
192
193 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
194 """
195 Old hook name: keep here for backward compatibility.
196
197 This is only required when the installed git hooks are not upgraded.
198 """
199 pass
200
201
202 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
203 """
204 Old hook name: keep here for backward compatibility.
205
206 This is only required when the installed git hooks are not upgraded.
207 """
208 pass
209
210
211 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
212
213
214 def git_pre_pull(extras):
215 """
216 Pre pull hook.
217
218 :param extras: dictionary containing the keys defined in simplevcs
219 :type extras: dict
220
221 :return: status code of the hook. 0 for success.
222 :rtype: int
223 """
224 if 'pull' not in extras['hooks']:
225 return HookResponse(0, '')
226
227 stdout = io.BytesIO()
228 try:
229 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
230 except Exception as error:
231 status = 128
232 stdout.write('ERROR: %s\n' % str(error))
233
234 return HookResponse(status, stdout.getvalue())
235
236
237 def git_post_pull(extras):
238 """
239 Post pull hook.
240
241 :param extras: dictionary containing the keys defined in simplevcs
242 :type extras: dict
243
244 :return: status code of the hook. 0 for success.
245 :rtype: int
246 """
247 if 'pull' not in extras['hooks']:
248 return HookResponse(0, '')
249
250 stdout = io.BytesIO()
251 try:
252 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
253 except Exception as error:
254 status = 128
255 stdout.write('ERROR: %s\n' % error)
256
257 return HookResponse(status, stdout.getvalue())
258
259
260 def git_pre_receive(unused_repo_path, unused_revs, env):
261 """
262 Pre push hook.
263
264 :param extras: dictionary containing the keys defined in simplevcs
265 :type extras: dict
266
267 :return: status code of the hook. 0 for success.
268 :rtype: int
269 """
270 extras = json.loads(env['RC_SCM_DATA'])
271 if 'push' not in extras['hooks']:
272 return 0
273 return _call_hook('pre_push', extras, GitMessageWriter())
274
275
276 def _run_command(arguments):
277 """
278 Run the specified command and return the stdout.
279
280 :param arguments: sequence of program arugments (including the program name)
281 :type arguments: list[str]
282 """
283 # TODO(skreft): refactor this method and all the other similar ones.
284 # Probably this should be using subprocessio.
285 process = subprocess.Popen(
286 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
287 stdout, _ = process.communicate()
288
289 if process.returncode != 0:
290 raise Exception(
291 'Command %s exited with exit code %s' % (arguments,
292 process.returncode))
293
294 return stdout
295
296
297 def git_post_receive(unused_repo_path, revision_lines, env):
298 """
299 Post push hook.
300
301 :param extras: dictionary containing the keys defined in simplevcs
302 :type extras: dict
303
304 :return: status code of the hook. 0 for success.
305 :rtype: int
306 """
307 extras = json.loads(env['RC_SCM_DATA'])
308 if 'push' not in extras['hooks']:
309 return 0
310
311 rev_data = []
312 for revision_line in revision_lines:
313 old_rev, new_rev, ref = revision_line.strip().split(' ')
314 ref_data = ref.split('/', 2)
315 if ref_data[1] in ('tags', 'heads'):
316 rev_data.append({
317 'old_rev': old_rev,
318 'new_rev': new_rev,
319 'ref': ref,
320 'type': ref_data[1],
321 'name': ref_data[2],
322 })
323
324 git_revs = []
325
326 # N.B.(skreft): it is ok to just call git, as git before calling a
327 # subcommand sets the PATH environment variable so that it point to the
328 # correct version of the git executable.
329 empty_commit_id = '0' * 40
330 for push_ref in rev_data:
331 type_ = push_ref['type']
332 if type_ == 'heads':
333 if push_ref['old_rev'] == empty_commit_id:
334
335 # Fix up head revision if needed
336 cmd = ['git', 'show', 'HEAD']
337 try:
338 _run_command(cmd)
339 except Exception:
340 cmd = ['git', 'symbolic-ref', 'HEAD',
341 'refs/heads/%s' % push_ref['name']]
342 print "Setting default branch to %s" % push_ref['name']
343 _run_command(cmd)
344
345 cmd = ['git', 'for-each-ref', '--format=%(refname)',
346 'refs/heads/*']
347 heads = _run_command(cmd)
348 heads = heads.replace(push_ref['ref'], '')
349 heads = ' '.join(head for head in heads.splitlines() if head)
350 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
351 '--', push_ref['new_rev'], '--not', heads]
352 git_revs.extend(_run_command(cmd).splitlines())
353 elif push_ref['new_rev'] == empty_commit_id:
354 # delete branch case
355 git_revs.append('delete_branch=>%s' % push_ref['name'])
356 else:
357 cmd = ['git', 'log',
358 '{old_rev}..{new_rev}'.format(**push_ref),
359 '--reverse', '--pretty=format:%H']
360 git_revs.extend(_run_command(cmd).splitlines())
361 elif type_ == 'tags':
362 git_revs.append('tag=>%s' % push_ref['name'])
363
364 extras['commit_ids'] = git_revs
365
366 if 'repo_size' in extras['hooks']:
367 try:
368 _call_hook('repo_size', extras, GitMessageWriter())
369 except:
370 pass
371
372 return _call_hook('post_push', extras, GitMessageWriter())
@@ -0,0 +1,335 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import base64
19 import locale
20 import logging
21 import uuid
22 import wsgiref.util
23 from itertools import chain
24
25 import msgpack
26 from beaker.cache import CacheManager
27 from beaker.util import parse_cache_config_options
28 from pyramid.config import Configurator
29 from pyramid.wsgi import wsgiapp
30
31 from vcsserver import remote_wsgi, scm_app, settings
32 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
33 from vcsserver.echo_stub.echo_app import EchoApp
34 from vcsserver.server import VcsServer
35
36 try:
37 from vcsserver.git import GitFactory, GitRemote
38 except ImportError:
39 GitFactory = None
40 GitRemote = None
41 try:
42 from vcsserver.hg import MercurialFactory, HgRemote
43 except ImportError:
44 MercurialFactory = None
45 HgRemote = None
46 try:
47 from vcsserver.svn import SubversionFactory, SvnRemote
48 except ImportError:
49 SubversionFactory = None
50 SvnRemote = None
51
52 log = logging.getLogger(__name__)
53
54
55 class VCS(object):
56 def __init__(self, locale=None, cache_config=None):
57 self.locale = locale
58 self.cache_config = cache_config
59 self._configure_locale()
60 self._initialize_cache()
61
62 if GitFactory and GitRemote:
63 git_repo_cache = self.cache.get_cache_region(
64 'git', region='repo_object')
65 git_factory = GitFactory(git_repo_cache)
66 self._git_remote = GitRemote(git_factory)
67 else:
68 log.info("Git client import failed")
69
70 if MercurialFactory and HgRemote:
71 hg_repo_cache = self.cache.get_cache_region(
72 'hg', region='repo_object')
73 hg_factory = MercurialFactory(hg_repo_cache)
74 self._hg_remote = HgRemote(hg_factory)
75 else:
76 log.info("Mercurial client import failed")
77
78 if SubversionFactory and SvnRemote:
79 svn_repo_cache = self.cache.get_cache_region(
80 'svn', region='repo_object')
81 svn_factory = SubversionFactory(svn_repo_cache)
82 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
83 else:
84 log.info("Subversion client import failed")
85
86 self._vcsserver = VcsServer()
87
88 def _initialize_cache(self):
89 cache_config = parse_cache_config_options(self.cache_config)
90 log.info('Initializing beaker cache: %s' % cache_config)
91 self.cache = CacheManager(**cache_config)
92
93 def _configure_locale(self):
94 if self.locale:
95 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
96 else:
97 log.info(
98 'Configuring locale subsystem based on environment variables')
99 try:
100 # If self.locale is the empty string, then the locale
101 # module will use the environment variables. See the
102 # documentation of the package `locale`.
103 locale.setlocale(locale.LC_ALL, self.locale)
104
105 language_code, encoding = locale.getlocale()
106 log.info(
107 'Locale set to language code "%s" with encoding "%s".',
108 language_code, encoding)
109 except locale.Error:
110 log.exception(
111 'Cannot set locale, not configuring the locale system')
112
113
114 class WsgiProxy(object):
115 def __init__(self, wsgi):
116 self.wsgi = wsgi
117
118 def __call__(self, environ, start_response):
119 input_data = environ['wsgi.input'].read()
120 input_data = msgpack.unpackb(input_data)
121
122 error = None
123 try:
124 data, status, headers = self.wsgi.handle(
125 input_data['environment'], input_data['input_data'],
126 *input_data['args'], **input_data['kwargs'])
127 except Exception as e:
128 data, status, headers = [], None, None
129 error = {
130 'message': str(e),
131 '_vcs_kind': getattr(e, '_vcs_kind', None)
132 }
133
134 start_response(200, {})
135 return self._iterator(error, status, headers, data)
136
137 def _iterator(self, error, status, headers, data):
138 initial_data = [
139 error,
140 status,
141 headers,
142 ]
143
144 for d in chain(initial_data, data):
145 yield msgpack.packb(d)
146
147
148 class HTTPApplication(object):
149 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
150
151 remote_wsgi = remote_wsgi
152 _use_echo_app = False
153
154 def __init__(self, settings=None):
155 self.config = Configurator(settings=settings)
156 locale = settings.get('', 'en_US.UTF-8')
157 vcs = VCS(locale=locale, cache_config=settings)
158 self._remotes = {
159 'hg': vcs._hg_remote,
160 'git': vcs._git_remote,
161 'svn': vcs._svn_remote,
162 'server': vcs._vcsserver,
163 }
164 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
165 self._use_echo_app = True
166 log.warning("Using EchoApp for VCS operations.")
167 self.remote_wsgi = remote_wsgi_stub
168 self._configure_settings(settings)
169 self._configure()
170
171 def _configure_settings(self, app_settings):
172 """
173 Configure the settings module.
174 """
175 git_path = app_settings.get('git_path', None)
176 if git_path:
177 settings.GIT_EXECUTABLE = git_path
178
179 def _configure(self):
180 self.config.add_renderer(
181 name='msgpack',
182 factory=self._msgpack_renderer_factory)
183
184 self.config.add_route('status', '/status')
185 self.config.add_route('hg_proxy', '/proxy/hg')
186 self.config.add_route('git_proxy', '/proxy/git')
187 self.config.add_route('vcs', '/{backend}')
188 self.config.add_route('stream_git', '/stream/git/*repo_name')
189 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
190
191 self.config.add_view(
192 self.status_view, route_name='status', renderer='json')
193 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
194 self.config.add_view(self.git_proxy(), route_name='git_proxy')
195 self.config.add_view(
196 self.vcs_view, route_name='vcs', renderer='msgpack')
197
198 self.config.add_view(self.hg_stream(), route_name='stream_hg')
199 self.config.add_view(self.git_stream(), route_name='stream_git')
200
201 def wsgi_app(self):
202 return self.config.make_wsgi_app()
203
204 def vcs_view(self, request):
205 remote = self._remotes[request.matchdict['backend']]
206 payload = msgpack.unpackb(request.body, use_list=True)
207 method = payload.get('method')
208 params = payload.get('params')
209 wire = params.get('wire')
210 args = params.get('args')
211 kwargs = params.get('kwargs')
212 if wire:
213 try:
214 wire['context'] = uuid.UUID(wire['context'])
215 except KeyError:
216 pass
217 args.insert(0, wire)
218
219 try:
220 resp = getattr(remote, method)(*args, **kwargs)
221 except Exception as e:
222 type_ = e.__class__.__name__
223 if type_ not in self.ALLOWED_EXCEPTIONS:
224 type_ = None
225
226 resp = {
227 'id': payload.get('id'),
228 'error': {
229 'message': e.message,
230 'type': type_
231 }
232 }
233 try:
234 resp['error']['_vcs_kind'] = e._vcs_kind
235 except AttributeError:
236 pass
237 else:
238 resp = {
239 'id': payload.get('id'),
240 'result': resp
241 }
242
243 return resp
244
245 def status_view(self, request):
246 return {'status': 'OK'}
247
248 def _msgpack_renderer_factory(self, info):
249 def _render(value, system):
250 value = msgpack.packb(value)
251 request = system.get('request')
252 if request is not None:
253 response = request.response
254 ct = response.content_type
255 if ct == response.default_content_type:
256 response.content_type = 'application/x-msgpack'
257 return value
258 return _render
259
260 def hg_proxy(self):
261 @wsgiapp
262 def _hg_proxy(environ, start_response):
263 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
264 return app(environ, start_response)
265 return _hg_proxy
266
267 def git_proxy(self):
268 @wsgiapp
269 def _git_proxy(environ, start_response):
270 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
271 return app(environ, start_response)
272 return _git_proxy
273
274 def hg_stream(self):
275 if self._use_echo_app:
276 @wsgiapp
277 def _hg_stream(environ, start_response):
278 app = EchoApp('fake_path', 'fake_name', None)
279 return app(environ, start_response)
280 return _hg_stream
281 else:
282 @wsgiapp
283 def _hg_stream(environ, start_response):
284 repo_path = environ['HTTP_X_RC_REPO_PATH']
285 repo_name = environ['HTTP_X_RC_REPO_NAME']
286 packed_config = base64.b64decode(
287 environ['HTTP_X_RC_REPO_CONFIG'])
288 config = msgpack.unpackb(packed_config)
289 app = scm_app.create_hg_wsgi_app(
290 repo_path, repo_name, config)
291
292 # Consitent path information for hgweb
293 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
294 environ['REPO_NAME'] = repo_name
295 return app(environ, ResponseFilter(start_response))
296 return _hg_stream
297
298 def git_stream(self):
299 if self._use_echo_app:
300 @wsgiapp
301 def _git_stream(environ, start_response):
302 app = EchoApp('fake_path', 'fake_name', None)
303 return app(environ, start_response)
304 return _git_stream
305 else:
306 @wsgiapp
307 def _git_stream(environ, start_response):
308 repo_path = environ['HTTP_X_RC_REPO_PATH']
309 repo_name = environ['HTTP_X_RC_REPO_NAME']
310 packed_config = base64.b64decode(
311 environ['HTTP_X_RC_REPO_CONFIG'])
312 config = msgpack.unpackb(packed_config)
313
314 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
315 app = scm_app.create_git_wsgi_app(
316 repo_path, repo_name, config)
317 return app(environ, start_response)
318 return _git_stream
319
320
321 class ResponseFilter(object):
322
323 def __init__(self, start_response):
324 self._start_response = start_response
325
326 def __call__(self, status, response_headers, exc_info=None):
327 headers = tuple(
328 (h, v) for h, v in response_headers
329 if not wsgiref.util.is_hop_by_hop(h))
330 return self._start_response(status, headers, exc_info)
331
332
333 def main(global_config, **settings):
334 app = HTTPApplication(settings=settings)
335 return app.wsgi_app()
This diff has been collapsed as it changes many lines, (507 lines changed) Show them Hide them
@@ -0,0 +1,507 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import atexit
19 import locale
20 import logging
21 import optparse
22 import os
23 import textwrap
24 import threading
25 import sys
26
27 import configobj
28 import Pyro4
29 from beaker.cache import CacheManager
30 from beaker.util import parse_cache_config_options
31
32 try:
33 from vcsserver.git import GitFactory, GitRemote
34 except ImportError:
35 GitFactory = None
36 GitRemote = None
37 try:
38 from vcsserver.hg import MercurialFactory, HgRemote
39 except ImportError:
40 MercurialFactory = None
41 HgRemote = None
42 try:
43 from vcsserver.svn import SubversionFactory, SvnRemote
44 except ImportError:
45 SubversionFactory = None
46 SvnRemote = None
47
48 from server import VcsServer
49 from vcsserver import hgpatches, remote_wsgi, settings
50 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
51
52 log = logging.getLogger(__name__)
53
54 HERE = os.path.dirname(os.path.abspath(__file__))
55 SERVER_RUNNING_FILE = None
56
57
58 # HOOKS - inspired by gunicorn #
59
60 def when_ready(server):
61 """
62 Called just after the server is started.
63 """
64
65 def _remove_server_running_file():
66 if os.path.isfile(SERVER_RUNNING_FILE):
67 os.remove(SERVER_RUNNING_FILE)
68
69 # top up to match to level location
70 if SERVER_RUNNING_FILE:
71 with open(SERVER_RUNNING_FILE, 'wb') as f:
72 f.write(str(os.getpid()))
73 # register cleanup of that file when server exits
74 atexit.register(_remove_server_running_file)
75
76
77 class LazyWriter(object):
78 """
79 File-like object that opens a file lazily when it is first written
80 to.
81 """
82
83 def __init__(self, filename, mode='w'):
84 self.filename = filename
85 self.fileobj = None
86 self.lock = threading.Lock()
87 self.mode = mode
88
89 def open(self):
90 if self.fileobj is None:
91 with self.lock:
92 self.fileobj = open(self.filename, self.mode)
93 return self.fileobj
94
95 def close(self):
96 fileobj = self.fileobj
97 if fileobj is not None:
98 fileobj.close()
99
100 def __del__(self):
101 self.close()
102
103 def write(self, text):
104 fileobj = self.open()
105 fileobj.write(text)
106 fileobj.flush()
107
108 def writelines(self, text):
109 fileobj = self.open()
110 fileobj.writelines(text)
111 fileobj.flush()
112
113 def flush(self):
114 self.open().flush()
115
116
117 class Application(object):
118 """
119 Represents the vcs server application.
120
121 This object is responsible to initialize the application and all needed
122 libraries. After that it hooks together the different objects and provides
123 them a way to access things like configuration.
124 """
125
126 def __init__(
127 self, host, port=None, locale='', threadpool_size=None,
128 timeout=None, cache_config=None, remote_wsgi_=None):
129
130 self.host = host
131 self.port = int(port) or settings.PYRO_PORT
132 self.threadpool_size = (
133 int(threadpool_size) if threadpool_size else None)
134 self.locale = locale
135 self.timeout = timeout
136 self.cache_config = cache_config
137 self.remote_wsgi = remote_wsgi_ or remote_wsgi
138
139 def init(self):
140 """
141 Configure and hook together all relevant objects.
142 """
143 self._configure_locale()
144 self._configure_pyro()
145 self._initialize_cache()
146 self._create_daemon_and_remote_objects(host=self.host, port=self.port)
147
148 def run(self):
149 """
150 Start the main loop of the application.
151 """
152
153 if hasattr(os, 'getpid'):
154 log.info('Starting %s in PID %i.', __name__, os.getpid())
155 else:
156 log.info('Starting %s.', __name__)
157 if SERVER_RUNNING_FILE:
158 log.info('PID file written as %s', SERVER_RUNNING_FILE)
159 else:
160 log.info('No PID file written by default.')
161 when_ready(self)
162 try:
163 self._pyrodaemon.requestLoop(
164 loopCondition=lambda: not self._vcsserver._shutdown)
165 finally:
166 self._pyrodaemon.shutdown()
167
168 def _configure_locale(self):
169 if self.locale:
170 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
171 else:
172 log.info(
173 'Configuring locale subsystem based on environment variables')
174
175 try:
176 # If self.locale is the empty string, then the locale
177 # module will use the environment variables. See the
178 # documentation of the package `locale`.
179 locale.setlocale(locale.LC_ALL, self.locale)
180
181 language_code, encoding = locale.getlocale()
182 log.info(
183 'Locale set to language code "%s" with encoding "%s".',
184 language_code, encoding)
185 except locale.Error:
186 log.exception(
187 'Cannot set locale, not configuring the locale system')
188
189 def _configure_pyro(self):
190 if self.threadpool_size is not None:
191 log.info("Threadpool size set to %s", self.threadpool_size)
192 Pyro4.config.THREADPOOL_SIZE = self.threadpool_size
193 if self.timeout not in (None, 0, 0.0, '0'):
194 log.info("Timeout for RPC calls set to %s seconds", self.timeout)
195 Pyro4.config.COMMTIMEOUT = float(self.timeout)
196 Pyro4.config.SERIALIZER = 'pickle'
197 Pyro4.config.SERIALIZERS_ACCEPTED.add('pickle')
198 Pyro4.config.SOCK_REUSE = True
199 # Uncomment the next line when you need to debug remote errors
200 # Pyro4.config.DETAILED_TRACEBACK = True
201
202 def _initialize_cache(self):
203 cache_config = parse_cache_config_options(self.cache_config)
204 log.info('Initializing beaker cache: %s' % cache_config)
205 self.cache = CacheManager(**cache_config)
206
207 def _create_daemon_and_remote_objects(self, host='localhost',
208 port=settings.PYRO_PORT):
209 daemon = Pyro4.Daemon(host=host, port=port)
210
211 self._vcsserver = VcsServer()
212 uri = daemon.register(
213 self._vcsserver, objectId=settings.PYRO_VCSSERVER)
214 log.info("Object registered = %s", uri)
215
216 if GitFactory and GitRemote:
217 git_repo_cache = self.cache.get_cache_region('git', region='repo_object')
218 git_factory = GitFactory(git_repo_cache)
219 self._git_remote = GitRemote(git_factory)
220 uri = daemon.register(self._git_remote, objectId=settings.PYRO_GIT)
221 log.info("Object registered = %s", uri)
222 else:
223 log.info("Git client import failed")
224
225 if MercurialFactory and HgRemote:
226 hg_repo_cache = self.cache.get_cache_region('hg', region='repo_object')
227 hg_factory = MercurialFactory(hg_repo_cache)
228 self._hg_remote = HgRemote(hg_factory)
229 uri = daemon.register(self._hg_remote, objectId=settings.PYRO_HG)
230 log.info("Object registered = %s", uri)
231 else:
232 log.info("Mercurial client import failed")
233
234 if SubversionFactory and SvnRemote:
235 svn_repo_cache = self.cache.get_cache_region('svn', region='repo_object')
236 svn_factory = SubversionFactory(svn_repo_cache)
237 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
238 uri = daemon.register(self._svn_remote, objectId=settings.PYRO_SVN)
239 log.info("Object registered = %s", uri)
240 else:
241 log.info("Subversion client import failed")
242
243 self._git_remote_wsgi = self.remote_wsgi.GitRemoteWsgi()
244 uri = daemon.register(self._git_remote_wsgi,
245 objectId=settings.PYRO_GIT_REMOTE_WSGI)
246 log.info("Object registered = %s", uri)
247
248 self._hg_remote_wsgi = self.remote_wsgi.HgRemoteWsgi()
249 uri = daemon.register(self._hg_remote_wsgi,
250 objectId=settings.PYRO_HG_REMOTE_WSGI)
251 log.info("Object registered = %s", uri)
252
253 self._pyrodaemon = daemon
254
255
256 class VcsServerCommand(object):
257
258 usage = '%prog'
259 description = """
260 Runs the VCS server
261 """
262 default_verbosity = 1
263
264 parser = optparse.OptionParser(
265 usage,
266 description=textwrap.dedent(description)
267 )
268 parser.add_option(
269 '--host',
270 type="str",
271 dest="host",
272 )
273 parser.add_option(
274 '--port',
275 type="int",
276 dest="port"
277 )
278 parser.add_option(
279 '--running-file',
280 dest='running_file',
281 metavar='RUNNING_FILE',
282 help="Create a running file after the server is initalized with "
283 "stored PID of process"
284 )
285 parser.add_option(
286 '--locale',
287 dest='locale',
288 help="Allows to set the locale, e.g. en_US.UTF-8",
289 default=""
290 )
291 parser.add_option(
292 '--log-file',
293 dest='log_file',
294 metavar='LOG_FILE',
295 help="Save output to the given log file (redirects stdout)"
296 )
297 parser.add_option(
298 '--log-level',
299 dest="log_level",
300 metavar="LOG_LEVEL",
301 help="use LOG_LEVEL to set log level "
302 "(debug,info,warning,error,critical)"
303 )
304 parser.add_option(
305 '--threadpool',
306 dest='threadpool_size',
307 type='int',
308 help="Set the size of the threadpool used to communicate with the "
309 "WSGI workers. This should be at least 6 times the number of "
310 "WSGI worker processes."
311 )
312 parser.add_option(
313 '--timeout',
314 dest='timeout',
315 type='float',
316 help="Set the timeout for RPC communication in seconds."
317 )
318 parser.add_option(
319 '--config',
320 dest='config_file',
321 type='string',
322 help="Configuration file for vcsserver."
323 )
324
325 def __init__(self, argv, quiet=False):
326 self.options, self.args = self.parser.parse_args(argv[1:])
327 if quiet:
328 self.options.verbose = 0
329
330 def _get_file_config(self):
331 ini_conf = {}
332 conf = configobj.ConfigObj(self.options.config_file)
333 if 'DEFAULT' in conf:
334 ini_conf = conf['DEFAULT']
335
336 return ini_conf
337
338 def _show_config(self, vcsserver_config):
339 order = [
340 'config_file',
341 'host',
342 'port',
343 'log_file',
344 'log_level',
345 'locale',
346 'threadpool_size',
347 'timeout',
348 'cache_config',
349 ]
350
351 def sorter(k):
352 return dict([(y, x) for x, y in enumerate(order)]).get(k)
353
354 _config = []
355 for k in sorted(vcsserver_config.keys(), key=sorter):
356 v = vcsserver_config[k]
357 # construct padded key for display eg %-20s % = key: val
358 k_formatted = ('%-'+str(len(max(order, key=len))+1)+'s') % (k+':')
359 _config.append(' * %s %s' % (k_formatted, v))
360 log.info('\n[vcsserver configuration]:\n'+'\n'.join(_config))
361
362 def _get_vcsserver_configuration(self):
363 _defaults = {
364 'config_file': None,
365 'git_path': 'git',
366 'host': 'localhost',
367 'port': settings.PYRO_PORT,
368 'log_file': None,
369 'log_level': 'debug',
370 'locale': None,
371 'threadpool_size': 16,
372 'timeout': None,
373
374 # Development support
375 'dev.use_echo_app': False,
376
377 # caches, baker style config
378 'beaker.cache.regions': 'repo_object',
379 'beaker.cache.repo_object.expire': '10',
380 'beaker.cache.repo_object.type': 'memory',
381 }
382 config = {}
383 config.update(_defaults)
384 # overwrite defaults with one loaded from file
385 config.update(self._get_file_config())
386
387 # overwrite with self.option which has the top priority
388 for k, v in self.options.__dict__.items():
389 if v or v == 0:
390 config[k] = v
391
392 # clear all "extra" keys if they are somehow passed,
393 # we only want defaults, so any extra stuff from self.options is cleared
394 # except beaker stuff which needs to be dynamic
395 for k in [k for k in config.copy().keys() if not k.startswith('beaker.cache.')]:
396 if k not in _defaults:
397 del config[k]
398
399 # group together the cache into one key.
400 # Needed further for beaker lib configuration
401 _k = {}
402 for k in [k for k in config.copy() if k.startswith('beaker.cache.')]:
403 _k[k] = config.pop(k)
404 config['cache_config'] = _k
405
406 return config
407
408 def out(self, msg): # pragma: no cover
409 if self.options.verbose > 0:
410 print(msg)
411
412 def run(self): # pragma: no cover
413 vcsserver_config = self._get_vcsserver_configuration()
414
415 # Ensure the log file is writeable
416 if vcsserver_config['log_file']:
417 stdout_log = self._configure_logfile()
418 else:
419 stdout_log = None
420
421 # set PID file with running lock
422 if self.options.running_file:
423 global SERVER_RUNNING_FILE
424 SERVER_RUNNING_FILE = self.options.running_file
425
426 # configure logging, and logging based on configuration file
427 self._configure_logging(level=vcsserver_config['log_level'],
428 stream=stdout_log)
429 if self.options.config_file:
430 if not os.path.isfile(self.options.config_file):
431 raise OSError('File %s does not exist' %
432 self.options.config_file)
433
434 self._configure_file_logging(self.options.config_file)
435
436 self._configure_settings(vcsserver_config)
437
438 # display current configuration of vcsserver
439 self._show_config(vcsserver_config)
440
441 if not vcsserver_config['dev.use_echo_app']:
442 remote_wsgi_mod = remote_wsgi
443 else:
444 log.warning("Using EchoApp for VCS endpoints.")
445 remote_wsgi_mod = remote_wsgi_stub
446
447 app = Application(
448 host=vcsserver_config['host'],
449 port=vcsserver_config['port'],
450 locale=vcsserver_config['locale'],
451 threadpool_size=vcsserver_config['threadpool_size'],
452 timeout=vcsserver_config['timeout'],
453 cache_config=vcsserver_config['cache_config'],
454 remote_wsgi_=remote_wsgi_mod)
455 app.init()
456 app.run()
457
458 def _configure_logging(self, level, stream=None):
459 _format = (
460 '%(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s')
461 levels = {
462 'debug': logging.DEBUG,
463 'info': logging.INFO,
464 'warning': logging.WARNING,
465 'error': logging.ERROR,
466 'critical': logging.CRITICAL,
467 }
468 try:
469 level = levels[level]
470 except KeyError:
471 raise AttributeError(
472 'Invalid log level please use one of %s' % (levels.keys(),))
473 logging.basicConfig(format=_format, stream=stream, level=level)
474 logging.getLogger('Pyro4').setLevel(level)
475
476 def _configure_file_logging(self, config):
477 import logging.config
478 try:
479 logging.config.fileConfig(config)
480 except Exception as e:
481 log.warning('Failed to configure logging based on given '
482 'config file. Error: %s' % e)
483
484 def _configure_logfile(self):
485 try:
486 writeable_log_file = open(self.options.log_file, 'a')
487 except IOError as ioe:
488 msg = 'Error: Unable to write to log file: %s' % ioe
489 raise ValueError(msg)
490 writeable_log_file.close()
491 stdout_log = LazyWriter(self.options.log_file, 'a')
492 sys.stdout = stdout_log
493 sys.stderr = stdout_log
494 return stdout_log
495
496 def _configure_settings(self, config):
497 """
498 Configure the settings module based on the given `config`.
499 """
500 settings.GIT_EXECUTABLE = config['git_path']
501
502
503 def main(argv=sys.argv, quiet=False):
504 if MercurialFactory:
505 hgpatches.patch_largefiles_capabilities()
506 command = VcsServerCommand(argv, quiet=quiet)
507 return command.run()
@@ -0,0 +1,375 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """Handles the Git smart protocol."""
19
20 import os
21 import socket
22 import logging
23
24 import simplejson as json
25 import dulwich.protocol
26 from webob import Request, Response, exc
27
28 from vcsserver import hooks, subprocessio
29
30
31 log = logging.getLogger(__name__)
32
33
34 class FileWrapper(object):
35 """File wrapper that ensures how much data is read from it."""
36
37 def __init__(self, fd, content_length):
38 self.fd = fd
39 self.content_length = content_length
40 self.remain = content_length
41
42 def read(self, size):
43 if size <= self.remain:
44 try:
45 data = self.fd.read(size)
46 except socket.error:
47 raise IOError(self)
48 self.remain -= size
49 elif self.remain:
50 data = self.fd.read(self.remain)
51 self.remain = 0
52 else:
53 data = None
54 return data
55
56 def __repr__(self):
57 return '<FileWrapper %s len: %s, read: %s>' % (
58 self.fd, self.content_length, self.content_length - self.remain
59 )
60
61
62 class GitRepository(object):
63 """WSGI app for handling Git smart protocol endpoints."""
64
65 git_folder_signature = frozenset(
66 ('config', 'head', 'info', 'objects', 'refs'))
67 commands = frozenset(('git-upload-pack', 'git-receive-pack'))
68 valid_accepts = frozenset(('application/x-%s-result' %
69 c for c in commands))
70
71 # The last bytes are the SHA1 of the first 12 bytes.
72 EMPTY_PACK = (
73 'PACK\x00\x00\x00\x02\x00\x00\x00\x00' +
74 '\x02\x9d\x08\x82;\xd8\xa8\xea\xb5\x10\xadj\xc7\\\x82<\xfd>\xd3\x1e'
75 )
76 SIDE_BAND_CAPS = frozenset(('side-band', 'side-band-64k'))
77
78 def __init__(self, repo_name, content_path, git_path, update_server_info,
79 extras):
80 files = frozenset(f.lower() for f in os.listdir(content_path))
81 valid_dir_signature = self.git_folder_signature.issubset(files)
82
83 if not valid_dir_signature:
84 raise OSError('%s missing git signature' % content_path)
85
86 self.content_path = content_path
87 self.repo_name = repo_name
88 self.extras = extras
89 self.git_path = git_path
90 self.update_server_info = update_server_info
91
92 def _get_fixedpath(self, path):
93 """
94 Small fix for repo_path
95
96 :param path:
97 """
98 return path.split(self.repo_name, 1)[-1].strip('/')
99
100 def inforefs(self, request, unused_environ):
101 """
102 WSGI Response producer for HTTP GET Git Smart
103 HTTP /info/refs request.
104 """
105
106 git_command = request.GET.get('service')
107 if git_command not in self.commands:
108 log.debug('command %s not allowed', git_command)
109 return exc.HTTPForbidden()
110
111 # please, resist the urge to add '\n' to git capture and increment
112 # line count by 1.
113 # by git docs: Documentation/technical/http-protocol.txt#L214 \n is
114 # a part of protocol.
115 # The code in Git client not only does NOT need '\n', but actually
116 # blows up if you sprinkle "flush" (0000) as "0001\n".
117 # It reads binary, per number of bytes specified.
118 # if you do add '\n' as part of data, count it.
119 server_advert = '# service=%s\n' % git_command
120 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
121 try:
122 gitenv = dict(os.environ)
123 # forget all configs
124 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
125 command = [self.git_path, git_command[4:], '--stateless-rpc',
126 '--advertise-refs', self.content_path]
127 out = subprocessio.SubprocessIOChunker(
128 command,
129 env=gitenv,
130 starting_values=[packet_len + server_advert + '0000'],
131 shell=False
132 )
133 except EnvironmentError:
134 log.exception('Error processing command')
135 raise exc.HTTPExpectationFailed()
136
137 resp = Response()
138 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
139 resp.charset = None
140 resp.app_iter = out
141
142 return resp
143
144 def _get_want_capabilities(self, request):
145 """Read the capabilities found in the first want line of the request."""
146 pos = request.body_file_seekable.tell()
147 first_line = request.body_file_seekable.readline()
148 request.body_file_seekable.seek(pos)
149
150 return frozenset(
151 dulwich.protocol.extract_want_line_capabilities(first_line)[1])
152
153 def _build_failed_pre_pull_response(self, capabilities, pre_pull_messages):
154 """
155 Construct a response with an empty PACK file.
156
157 We use an empty PACK file, as that would trigger the failure of the pull
158 or clone command.
159
160 We also print in the error output a message explaining why the command
161 was aborted.
162
163 If aditionally, the user is accepting messages we send them the output
164 of the pre-pull hook.
165
166 Note that for clients not supporting side-band we just send them the
167 emtpy PACK file.
168 """
169 if self.SIDE_BAND_CAPS.intersection(capabilities):
170 response = []
171 proto = dulwich.protocol.Protocol(None, response.append)
172 proto.write_pkt_line('NAK\n')
173 self._write_sideband_to_proto(pre_pull_messages, proto,
174 capabilities)
175 # N.B.(skreft): Do not change the sideband channel to 3, as that
176 # produces a fatal error in the client:
177 # fatal: error in sideband demultiplexer
178 proto.write_sideband(2, 'Pre pull hook failed: aborting\n')
179 proto.write_sideband(1, self.EMPTY_PACK)
180
181 # writes 0000
182 proto.write_pkt_line(None)
183
184 return response
185 else:
186 return [self.EMPTY_PACK]
187
188 def _write_sideband_to_proto(self, data, proto, capabilities):
189 """
190 Write the data to the proto's sideband number 2.
191
192 We do not use dulwich's write_sideband directly as it only supports
193 side-band-64k.
194 """
195 if not data:
196 return
197
198 # N.B.(skreft): The values below are explained in the pack protocol
199 # documentation, section Packfile Data.
200 # https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
201 if 'side-band-64k' in capabilities:
202 chunk_size = 65515
203 elif 'side-band' in capabilities:
204 chunk_size = 995
205 else:
206 return
207
208 chunker = (
209 data[i:i + chunk_size] for i in xrange(0, len(data), chunk_size))
210
211 for chunk in chunker:
212 proto.write_sideband(2, chunk)
213
214 def _get_messages(self, data, capabilities):
215 """Return a list with packets for sending data in sideband number 2."""
216 response = []
217 proto = dulwich.protocol.Protocol(None, response.append)
218
219 self._write_sideband_to_proto(data, proto, capabilities)
220
221 return response
222
223 def _inject_messages_to_response(self, response, capabilities,
224 start_messages, end_messages):
225 """
226 Given a list reponse we inject the pre/post-pull messages.
227
228 We only inject the messages if the client supports sideband, and the
229 response has the format:
230 0008NAK\n...0000
231
232 Note that we do not check the no-progress capability as by default, git
233 sends it, which effectively would block all messages.
234 """
235 if not self.SIDE_BAND_CAPS.intersection(capabilities):
236 return response
237
238 if (not response[0].startswith('0008NAK\n') or
239 not response[-1].endswith('0000')):
240 return response
241
242 if not start_messages and not end_messages:
243 return response
244
245 new_response = ['0008NAK\n']
246 new_response.extend(self._get_messages(start_messages, capabilities))
247 if len(response) == 1:
248 new_response.append(response[0][8:-4])
249 else:
250 new_response.append(response[0][8:])
251 new_response.extend(response[1:-1])
252 new_response.append(response[-1][:-4])
253 new_response.extend(self._get_messages(end_messages, capabilities))
254 new_response.append('0000')
255
256 return new_response
257
258 def backend(self, request, environ):
259 """
260 WSGI Response producer for HTTP POST Git Smart HTTP requests.
261 Reads commands and data from HTTP POST's body.
262 returns an iterator obj with contents of git command's
263 response to stdout
264 """
265 # TODO(skreft): think how we could detect an HTTPLockedException, as
266 # we probably want to have the same mechanism used by mercurial and
267 # simplevcs.
268 # For that we would need to parse the output of the command looking for
269 # some signs of the HTTPLockedError, parse the data and reraise it in
270 # pygrack. However, that would interfere with the streaming.
271 #
272 # Now the output of a blocked push is:
273 # Pushing to http://test_regular:test12@127.0.0.1:5001/vcs_test_git
274 # POST git-receive-pack (1047 bytes)
275 # remote: ERROR: Repository `vcs_test_git` locked by user `test_admin`. Reason:`lock_auto`
276 # To http://test_regular:test12@127.0.0.1:5001/vcs_test_git
277 # ! [remote rejected] master -> master (pre-receive hook declined)
278 # error: failed to push some refs to 'http://test_regular:test12@127.0.0.1:5001/vcs_test_git'
279
280 git_command = self._get_fixedpath(request.path_info)
281 if git_command not in self.commands:
282 log.debug('command %s not allowed', git_command)
283 return exc.HTTPForbidden()
284
285 capabilities = None
286 if git_command == 'git-upload-pack':
287 capabilities = self._get_want_capabilities(request)
288
289 if 'CONTENT_LENGTH' in environ:
290 inputstream = FileWrapper(request.body_file_seekable,
291 request.content_length)
292 else:
293 inputstream = request.body_file_seekable
294
295 resp = Response()
296 resp.content_type = ('application/x-%s-result' %
297 git_command.encode('utf8'))
298 resp.charset = None
299
300 if git_command == 'git-upload-pack':
301 status, pre_pull_messages = hooks.git_pre_pull(self.extras)
302 if status != 0:
303 resp.app_iter = self._build_failed_pre_pull_response(
304 capabilities, pre_pull_messages)
305 return resp
306
307 gitenv = dict(os.environ)
308 # forget all configs
309 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
310 gitenv['RC_SCM_DATA'] = json.dumps(self.extras)
311 cmd = [self.git_path, git_command[4:], '--stateless-rpc',
312 self.content_path]
313 log.debug('handling cmd %s', cmd)
314
315 out = subprocessio.SubprocessIOChunker(
316 cmd,
317 inputstream=inputstream,
318 env=gitenv,
319 cwd=self.content_path,
320 shell=False,
321 fail_on_stderr=False,
322 fail_on_return_code=False
323 )
324
325 if self.update_server_info and git_command == 'git-receive-pack':
326 # We need to fully consume the iterator here, as the
327 # update-server-info command needs to be run after the push.
328 out = list(out)
329
330 # Updating refs manually after each push.
331 # This is required as some clients are exposing Git repos internally
332 # with the dumb protocol.
333 cmd = [self.git_path, 'update-server-info']
334 log.debug('handling cmd %s', cmd)
335 output = subprocessio.SubprocessIOChunker(
336 cmd,
337 inputstream=inputstream,
338 env=gitenv,
339 cwd=self.content_path,
340 shell=False,
341 fail_on_stderr=False,
342 fail_on_return_code=False
343 )
344 # Consume all the output so the subprocess finishes
345 for _ in output:
346 pass
347
348 if git_command == 'git-upload-pack':
349 out = list(out)
350 unused_status, post_pull_messages = hooks.git_post_pull(self.extras)
351 resp.app_iter = self._inject_messages_to_response(
352 out, capabilities, pre_pull_messages, post_pull_messages)
353 else:
354 resp.app_iter = out
355
356 return resp
357
358 def __call__(self, environ, start_response):
359 request = Request(environ)
360 _path = self._get_fixedpath(request.path_info)
361 if _path.startswith('info/refs'):
362 app = self.inforefs
363 else:
364 app = self.backend
365
366 try:
367 resp = app(request, environ)
368 except exc.HTTPException as error:
369 log.exception('HTTP Error')
370 resp = error
371 except Exception:
372 log.exception('Unknown error')
373 resp = exc.HTTPInternalServerError()
374
375 return resp(environ, start_response)
@@ -0,0 +1,34 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 from vcsserver import scm_app, wsgi_app_caller
19
20
21 class GitRemoteWsgi(object):
22 def handle(self, environ, input_data, *args, **kwargs):
23 app = wsgi_app_caller.WSGIAppCaller(
24 scm_app.create_git_wsgi_app(*args, **kwargs))
25
26 return app.handle(environ, input_data)
27
28
29 class HgRemoteWsgi(object):
30 def handle(self, environ, input_data, *args, **kwargs):
31 app = wsgi_app_caller.WSGIAppCaller(
32 scm_app.create_hg_wsgi_app(*args, **kwargs))
33
34 return app.handle(environ, input_data)
@@ -0,0 +1,174 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import logging
19 import os
20
21 import mercurial
22 import mercurial.error
23 import mercurial.hgweb.common
24 import mercurial.hgweb.hgweb_mod
25 import mercurial.hgweb.protocol
26 import webob.exc
27
28 from vcsserver import pygrack, exceptions, settings
29
30
31 log = logging.getLogger(__name__)
32
33
34 # propagated from mercurial documentation
35 HG_UI_SECTIONS = [
36 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
37 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
38 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
39 ]
40
41
42 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
43 """Extension of hgweb that simplifies some functions."""
44
45 def _get_view(self, repo):
46 """Views are not supported."""
47 return repo
48
49 def loadsubweb(self):
50 """The result is only used in the templater method which is not used."""
51 return None
52
53 def run(self):
54 """Unused function so raise an exception if accidentally called."""
55 raise NotImplementedError
56
57 def templater(self, req):
58 """Function used in an unreachable code path.
59
60 This code is unreachable because we guarantee that the HTTP request,
61 corresponds to a Mercurial command. See the is_hg method. So, we are
62 never going to get a user-visible url.
63 """
64 raise NotImplementedError
65
66 def archivelist(self, nodeid):
67 """Unused function so raise an exception if accidentally called."""
68 raise NotImplementedError
69
70 def run_wsgi(self, req):
71 """Check the request has a valid command, failing fast otherwise."""
72 cmd = req.form.get('cmd', [''])[0]
73 if not mercurial.hgweb.protocol.iscmd(cmd):
74 req.respond(
75 mercurial.hgweb.common.ErrorResponse(
76 mercurial.hgweb.common.HTTP_BAD_REQUEST),
77 mercurial.hgweb.protocol.HGTYPE
78 )
79 return ['']
80
81 return super(HgWeb, self).run_wsgi(req)
82
83
84 def make_hg_ui_from_config(repo_config):
85 baseui = mercurial.ui.ui()
86
87 # clean the baseui object
88 baseui._ocfg = mercurial.config.config()
89 baseui._ucfg = mercurial.config.config()
90 baseui._tcfg = mercurial.config.config()
91
92 for section, option, value in repo_config:
93 baseui.setconfig(section, option, value)
94
95 # make our hgweb quiet so it doesn't print output
96 baseui.setconfig('ui', 'quiet', 'true')
97
98 return baseui
99
100
101 def update_hg_ui_from_hgrc(baseui, repo_path):
102 path = os.path.join(repo_path, '.hg', 'hgrc')
103
104 if not os.path.isfile(path):
105 log.debug('hgrc file is not present at %s, skipping...', path)
106 return
107 log.debug('reading hgrc from %s', path)
108 cfg = mercurial.config.config()
109 cfg.read(path)
110 for section in HG_UI_SECTIONS:
111 for k, v in cfg.items(section):
112 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
113 baseui.setconfig(section, k, v)
114
115
116 def create_hg_wsgi_app(repo_path, repo_name, config):
117 """
118 Prepares a WSGI application to handle Mercurial requests.
119
120 :param config: is a list of 3-item tuples representing a ConfigObject
121 (it is the serialized version of the config object).
122 """
123 log.debug("Creating Mercurial WSGI application")
124
125 baseui = make_hg_ui_from_config(config)
126 update_hg_ui_from_hgrc(baseui, repo_path)
127
128 try:
129 return HgWeb(repo_path, name=repo_name, baseui=baseui)
130 except mercurial.error.RequirementError as exc:
131 raise exceptions.RequirementException(exc)
132
133
134 class GitHandler(object):
135 def __init__(self, repo_location, repo_name, git_path, update_server_info,
136 extras):
137 if not os.path.isdir(repo_location):
138 raise OSError(repo_location)
139 self.content_path = repo_location
140 self.repo_name = repo_name
141 self.repo_location = repo_location
142 self.extras = extras
143 self.git_path = git_path
144 self.update_server_info = update_server_info
145
146 def __call__(self, environ, start_response):
147 app = webob.exc.HTTPNotFound()
148 candidate_paths = (
149 self.content_path, os.path.join(self.content_path, '.git'))
150
151 for content_path in candidate_paths:
152 try:
153 app = pygrack.GitRepository(
154 self.repo_name, content_path, self.git_path,
155 self.update_server_info, self.extras)
156 break
157 except OSError:
158 continue
159
160 return app(environ, start_response)
161
162
163 def create_git_wsgi_app(repo_path, repo_name, config):
164 """
165 Creates a WSGI application to handle Git requests.
166
167 :param config: is a dictionary holding the extras.
168 """
169 git_path = settings.GIT_EXECUTABLE
170 update_server_info = config.pop('git_update_server_info')
171 app = GitHandler(
172 repo_path, repo_name, git_path, update_server_info, config)
173
174 return app
@@ -0,0 +1,78 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 import gc
19 import logging
20 import os
21 import time
22
23
24 log = logging.getLogger(__name__)
25
26
27 class VcsServer(object):
28 """
29 Exposed remote interface of the vcsserver itself.
30
31 This object can be used to manage the server remotely. Right now the main
32 use case is to allow to shut down the server.
33 """
34
35 _shutdown = False
36
37 def shutdown(self):
38 self._shutdown = True
39
40 def ping(self):
41 """
42 Utility to probe a server connection.
43 """
44 log.debug("Received server ping.")
45
46 def echo(self, data):
47 """
48 Utility for performance testing.
49
50 Allows to pass in arbitrary data and will return this data.
51 """
52 log.debug("Received server echo.")
53 return data
54
55 def sleep(self, seconds):
56 """
57 Utility to simulate long running server interaction.
58 """
59 log.debug("Sleeping %s seconds", seconds)
60 time.sleep(seconds)
61
62 def get_pid(self):
63 """
64 Allows to discover the PID based on a proxy object.
65 """
66 return os.getpid()
67
68 def run_gc(self):
69 """
70 Allows to trigger the garbage collector.
71
72 Main intention is to support statistics gathering during test runs.
73 """
74 freed_objects = gc.collect()
75 return {
76 'freed_objects': freed_objects,
77 'garbage': len(gc.garbage),
78 }
@@ -0,0 +1,30 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18
19 PYRO_PORT = 9900
20
21 PYRO_GIT = 'git_remote'
22 PYRO_HG = 'hg_remote'
23 PYRO_SVN = 'svn_remote'
24 PYRO_VCSSERVER = 'vcs_server'
25 PYRO_GIT_REMOTE_WSGI = 'git_remote_wsgi'
26 PYRO_HG_REMOTE_WSGI = 'hg_remote_wsgi'
27
28 WIRE_ENCODING = 'UTF-8'
29
30 GIT_EXECUTABLE = 'git'
@@ -0,0 +1,476 b''
1 """
2 Module provides a class allowing to wrap communication over subprocess.Popen
3 input, output, error streams into a meaningfull, non-blocking, concurrent
4 stream processor exposing the output data as an iterator fitting to be a
5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
6
7 Copyright (c) 2011 Daniel Dotsenko <dotsa[at]hotmail.com>
8
9 This file is part of git_http_backend.py Project.
10
11 git_http_backend.py Project is free software: you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public License as
13 published by the Free Software Foundation, either version 2.1 of the License,
14 or (at your option) any later version.
15
16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU Lesser General Public License for more details.
20
21 You should have received a copy of the GNU Lesser General Public License
22 along with git_http_backend.py Project.
23 If not, see <http://www.gnu.org/licenses/>.
24 """
25 import os
26 import subprocess32 as subprocess
27 from collections import deque
28 from threading import Event, Thread
29
30
31 class StreamFeeder(Thread):
32 """
33 Normal writing into pipe-like is blocking once the buffer is filled.
34 This thread allows a thread to seep data from a file-like into a pipe
35 without blocking the main thread.
36 We close inpipe once the end of the source stream is reached.
37 """
38
39 def __init__(self, source):
40 super(StreamFeeder, self).__init__()
41 self.daemon = True
42 filelike = False
43 self.bytes = bytes()
44 if type(source) in (type(''), bytes, bytearray): # string-like
45 self.bytes = bytes(source)
46 else: # can be either file pointer or file-like
47 if type(source) in (int, long): # file pointer it is
48 ## converting file descriptor (int) stdin into file-like
49 try:
50 source = os.fdopen(source, 'rb', 16384)
51 except Exception:
52 pass
53 # let's see if source is file-like by now
54 try:
55 filelike = source.read
56 except Exception:
57 pass
58 if not filelike and not self.bytes:
59 raise TypeError("StreamFeeder's source object must be a readable "
60 "file-like, a file descriptor, or a string-like.")
61 self.source = source
62 self.readiface, self.writeiface = os.pipe()
63
64 def run(self):
65 t = self.writeiface
66 if self.bytes:
67 os.write(t, self.bytes)
68 else:
69 s = self.source
70 b = s.read(4096)
71 while b:
72 os.write(t, b)
73 b = s.read(4096)
74 os.close(t)
75
76 @property
77 def output(self):
78 return self.readiface
79
80
81 class InputStreamChunker(Thread):
82 def __init__(self, source, target, buffer_size, chunk_size):
83
84 super(InputStreamChunker, self).__init__()
85
86 self.daemon = True # die die die.
87
88 self.source = source
89 self.target = target
90 self.chunk_count_max = int(buffer_size / chunk_size) + 1
91 self.chunk_size = chunk_size
92
93 self.data_added = Event()
94 self.data_added.clear()
95
96 self.keep_reading = Event()
97 self.keep_reading.set()
98
99 self.EOF = Event()
100 self.EOF.clear()
101
102 self.go = Event()
103 self.go.set()
104
105 def stop(self):
106 self.go.clear()
107 self.EOF.set()
108 try:
109 # this is not proper, but is done to force the reader thread let
110 # go of the input because, if successful, .close() will send EOF
111 # down the pipe.
112 self.source.close()
113 except:
114 pass
115
116 def run(self):
117 s = self.source
118 t = self.target
119 cs = self.chunk_size
120 ccm = self.chunk_count_max
121 kr = self.keep_reading
122 da = self.data_added
123 go = self.go
124
125 try:
126 b = s.read(cs)
127 except ValueError:
128 b = ''
129
130 while b and go.is_set():
131 if len(t) > ccm:
132 kr.clear()
133 kr.wait(2)
134 # # this only works on 2.7.x and up
135 # if not kr.wait(10):
136 # raise Exception("Timed out while waiting for input to be read.")
137 # instead we'll use this
138 if len(t) > ccm + 3:
139 raise IOError(
140 "Timed out while waiting for input from subprocess.")
141 t.append(b)
142 da.set()
143 b = s.read(cs)
144 self.EOF.set()
145 da.set() # for cases when done but there was no input.
146
147
148 class BufferedGenerator(object):
149 """
150 Class behaves as a non-blocking, buffered pipe reader.
151 Reads chunks of data (through a thread)
152 from a blocking pipe, and attaches these to an array (Deque) of chunks.
153 Reading is halted in the thread when max chunks is internally buffered.
154 The .next() may operate in blocking or non-blocking fashion by yielding
155 '' if no data is ready
156 to be sent or by not returning until there is some data to send
157 When we get EOF from underlying source pipe we raise the marker to raise
158 StopIteration after the last chunk of data is yielded.
159 """
160
161 def __init__(self, source, buffer_size=65536, chunk_size=4096,
162 starting_values=[], bottomless=False):
163
164 if bottomless:
165 maxlen = int(buffer_size / chunk_size)
166 else:
167 maxlen = None
168
169 self.data = deque(starting_values, maxlen)
170 self.worker = InputStreamChunker(source, self.data, buffer_size,
171 chunk_size)
172 if starting_values:
173 self.worker.data_added.set()
174 self.worker.start()
175
176 ####################
177 # Generator's methods
178 ####################
179
180 def __iter__(self):
181 return self
182
183 def next(self):
184 while not len(self.data) and not self.worker.EOF.is_set():
185 self.worker.data_added.clear()
186 self.worker.data_added.wait(0.2)
187 if len(self.data):
188 self.worker.keep_reading.set()
189 return bytes(self.data.popleft())
190 elif self.worker.EOF.is_set():
191 raise StopIteration
192
193 def throw(self, type, value=None, traceback=None):
194 if not self.worker.EOF.is_set():
195 raise type(value)
196
197 def start(self):
198 self.worker.start()
199
200 def stop(self):
201 self.worker.stop()
202
203 def close(self):
204 try:
205 self.worker.stop()
206 self.throw(GeneratorExit)
207 except (GeneratorExit, StopIteration):
208 pass
209
210 def __del__(self):
211 self.close()
212
213 ####################
214 # Threaded reader's infrastructure.
215 ####################
216 @property
217 def input(self):
218 return self.worker.w
219
220 @property
221 def data_added_event(self):
222 return self.worker.data_added
223
224 @property
225 def data_added(self):
226 return self.worker.data_added.is_set()
227
228 @property
229 def reading_paused(self):
230 return not self.worker.keep_reading.is_set()
231
232 @property
233 def done_reading_event(self):
234 """
235 Done_reding does not mean that the iterator's buffer is empty.
236 Iterator might have done reading from underlying source, but the read
237 chunks might still be available for serving through .next() method.
238
239 :returns: An Event class instance.
240 """
241 return self.worker.EOF
242
243 @property
244 def done_reading(self):
245 """
246 Done_reding does not mean that the iterator's buffer is empty.
247 Iterator might have done reading from underlying source, but the read
248 chunks might still be available for serving through .next() method.
249
250 :returns: An Bool value.
251 """
252 return self.worker.EOF.is_set()
253
254 @property
255 def length(self):
256 """
257 returns int.
258
259 This is the lenght of the que of chunks, not the length of
260 the combined contents in those chunks.
261
262 __len__() cannot be meaningfully implemented because this
263 reader is just flying throuh a bottomless pit content and
264 can only know the lenght of what it already saw.
265
266 If __len__() on WSGI server per PEP 3333 returns a value,
267 the responce's length will be set to that. In order not to
268 confuse WSGI PEP3333 servers, we will not implement __len__
269 at all.
270 """
271 return len(self.data)
272
273 def prepend(self, x):
274 self.data.appendleft(x)
275
276 def append(self, x):
277 self.data.append(x)
278
279 def extend(self, o):
280 self.data.extend(o)
281
282 def __getitem__(self, i):
283 return self.data[i]
284
285
286 class SubprocessIOChunker(object):
287 """
288 Processor class wrapping handling of subprocess IO.
289
290 .. important::
291
292 Watch out for the method `__del__` on this class. If this object
293 is deleted, it will kill the subprocess, so avoid to
294 return the `output` attribute or usage of it like in the following
295 example::
296
297 # `args` expected to run a program that produces a lot of output
298 output = ''.join(SubprocessIOChunker(
299 args, shell=False, inputstream=inputstream, env=environ).output)
300
301 # `output` will not contain all the data, because the __del__ method
302 # has already killed the subprocess in this case before all output
303 # has been consumed.
304
305
306
307 In a way, this is a "communicate()" replacement with a twist.
308
309 - We are multithreaded. Writing in and reading out, err are all sep threads.
310 - We support concurrent (in and out) stream processing.
311 - The output is not a stream. It's a queue of read string (bytes, not unicode)
312 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
313 - We are non-blocking in more respects than communicate()
314 (reading from subprocess out pauses when internal buffer is full, but
315 does not block the parent calling code. On the flip side, reading from
316 slow-yielding subprocess may block the iteration until data shows up. This
317 does not block the parallel inpipe reading occurring parallel thread.)
318
319 The purpose of the object is to allow us to wrap subprocess interactions into
320 and interable that can be passed to a WSGI server as the application's return
321 value. Because of stream-processing-ability, WSGI does not have to read ALL
322 of the subprocess's output and buffer it, before handing it to WSGI server for
323 HTTP response. Instead, the class initializer reads just a bit of the stream
324 to figure out if error ocurred or likely to occur and if not, just hands the
325 further iteration over subprocess output to the server for completion of HTTP
326 response.
327
328 The real or perceived subprocess error is trapped and raised as one of
329 EnvironmentError family of exceptions
330
331 Example usage:
332 # try:
333 # answer = SubprocessIOChunker(
334 # cmd,
335 # input,
336 # buffer_size = 65536,
337 # chunk_size = 4096
338 # )
339 # except (EnvironmentError) as e:
340 # print str(e)
341 # raise e
342 #
343 # return answer
344
345
346 """
347
348 # TODO: johbo: This is used to make sure that the open end of the PIPE
349 # is closed in the end. It would be way better to wrap this into an
350 # object, so that it is closed automatically once it is consumed or
351 # something similar.
352 _close_input_fd = None
353
354 _closed = False
355
356 def __init__(self, cmd, inputstream=None, buffer_size=65536,
357 chunk_size=4096, starting_values=[], fail_on_stderr=True,
358 fail_on_return_code=True, **kwargs):
359 """
360 Initializes SubprocessIOChunker
361
362 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
363 :param inputstream: (Default: None) A file-like, string, or file pointer.
364 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
365 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
366 :param starting_values: (Default: []) An array of strings to put in front of output que.
367 :param fail_on_stderr: (Default: True) Whether to raise an exception in
368 case something is written to stderr.
369 :param fail_on_return_code: (Default: True) Whether to raise an
370 exception if the return code is not 0.
371 """
372
373 if inputstream:
374 input_streamer = StreamFeeder(inputstream)
375 input_streamer.start()
376 inputstream = input_streamer.output
377 self._close_input_fd = inputstream
378
379 self._fail_on_stderr = fail_on_stderr
380 self._fail_on_return_code = fail_on_return_code
381
382 _shell = kwargs.get('shell', True)
383 kwargs['shell'] = _shell
384
385 _p = subprocess.Popen(cmd, bufsize=-1,
386 stdin=inputstream,
387 stdout=subprocess.PIPE,
388 stderr=subprocess.PIPE,
389 **kwargs)
390
391 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size,
392 starting_values)
393 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
394
395 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
396 # doing this until we reach either end of file, or end of buffer.
397 bg_out.data_added_event.wait(1)
398 bg_out.data_added_event.clear()
399
400 # at this point it's still ambiguous if we are done reading or just full buffer.
401 # Either way, if error (returned by ended process, or implied based on
402 # presence of stuff in stderr output) we error out.
403 # Else, we are happy.
404 _returncode = _p.poll()
405
406 if ((_returncode and fail_on_return_code) or
407 (fail_on_stderr and _returncode is None and bg_err.length)):
408 try:
409 _p.terminate()
410 except Exception:
411 pass
412 bg_out.stop()
413 bg_err.stop()
414 if fail_on_stderr:
415 err = ''.join(bg_err)
416 raise EnvironmentError(
417 "Subprocess exited due to an error:\n" + err)
418 if _returncode and fail_on_return_code:
419 err = ''.join(bg_err)
420 raise EnvironmentError(
421 "Subprocess exited with non 0 ret code:%s: stderr:%s" % (
422 _returncode, err))
423
424 self.process = _p
425 self.output = bg_out
426 self.error = bg_err
427
428 def __iter__(self):
429 return self
430
431 def next(self):
432 # Note: mikhail: We need to be sure that we are checking the return
433 # code after the stdout stream is closed. Some processes, e.g. git
434 # are doing some magic in between closing stdout and terminating the
435 # process and, as a result, we are not getting return code on "slow"
436 # systems.
437 stop_iteration = None
438 try:
439 result = self.output.next()
440 except StopIteration as e:
441 stop_iteration = e
442
443 if self.process.poll() and self._fail_on_return_code:
444 err = '%s' % ''.join(self.error)
445 raise EnvironmentError(
446 "Subprocess exited due to an error:\n" + err)
447
448 if stop_iteration:
449 raise stop_iteration
450 return result
451
452 def throw(self, type, value=None, traceback=None):
453 if self.output.length or not self.output.done_reading:
454 raise type(value)
455
456 def close(self):
457 if self._closed:
458 return
459 self._closed = True
460 try:
461 self.process.terminate()
462 except:
463 pass
464 if self._close_input_fd:
465 os.close(self._close_input_fd)
466 try:
467 self.output.close()
468 except:
469 pass
470 try:
471 self.error.close()
472 except:
473 pass
474
475 def __del__(self):
476 self.close()
This diff has been collapsed as it changes many lines, (591 lines changed) Show them Hide them
@@ -0,0 +1,591 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 from __future__ import absolute_import
19
20 from urllib2 import URLError
21 import logging
22 import posixpath as vcspath
23 import StringIO
24 import subprocess
25 import urllib
26
27 import svn.client
28 import svn.core
29 import svn.delta
30 import svn.diff
31 import svn.fs
32 import svn.repos
33
34 from vcsserver import svn_diff
35 from vcsserver.base import RepoFactory
36
37
38 log = logging.getLogger(__name__)
39
40
41 # Set of svn compatible version flags.
42 # Compare with subversion/svnadmin/svnadmin.c
43 svn_compatible_versions = set([
44 'pre-1.4-compatible',
45 'pre-1.5-compatible',
46 'pre-1.6-compatible',
47 'pre-1.8-compatible',
48 ])
49
50
51 class SubversionFactory(RepoFactory):
52
53 def _create_repo(self, wire, create, compatible_version):
54 path = svn.core.svn_path_canonicalize(wire['path'])
55 if create:
56 fs_config = {}
57 if compatible_version:
58 if compatible_version not in svn_compatible_versions:
59 raise Exception('Unknown SVN compatible version "{}"'
60 .format(compatible_version))
61 log.debug('Create SVN repo with compatible version "%s"',
62 compatible_version)
63 fs_config[compatible_version] = '1'
64 repo = svn.repos.create(path, "", "", None, fs_config)
65 else:
66 repo = svn.repos.open(path)
67 return repo
68
69 def repo(self, wire, create=False, compatible_version=None):
70 def create_new_repo():
71 return self._create_repo(wire, create, compatible_version)
72
73 return self._repo(wire, create_new_repo)
74
75
76
77 NODE_TYPE_MAPPING = {
78 svn.core.svn_node_file: 'file',
79 svn.core.svn_node_dir: 'dir',
80 }
81
82
83 class SvnRemote(object):
84
85 def __init__(self, factory, hg_factory=None):
86 self._factory = factory
87 # TODO: Remove once we do not use internal Mercurial objects anymore
88 # for subversion
89 self._hg_factory = hg_factory
90
91 def check_url(self, url, config_items):
92 # this can throw exception if not installed, but we detect this
93 from hgsubversion import svnrepo
94
95 baseui = self._hg_factory._create_config(config_items)
96 # uuid function get's only valid UUID from proper repo, else
97 # throws exception
98 try:
99 svnrepo.svnremoterepo(baseui, url).svn.uuid
100 except:
101 log.debug("Invalid svn url: %s", url)
102 raise URLError(
103 '"%s" is not a valid Subversion source url.' % (url, ))
104 return True
105
106 def is_path_valid_repository(self, wire, path):
107 try:
108 svn.repos.open(path)
109 except svn.core.SubversionException:
110 log.debug("Invalid Subversion path %s", path)
111 return False
112 return True
113
114 def lookup(self, wire, revision):
115 if revision not in [-1, None, 'HEAD']:
116 raise NotImplementedError
117 repo = self._factory.repo(wire)
118 fs_ptr = svn.repos.fs(repo)
119 head = svn.fs.youngest_rev(fs_ptr)
120 return head
121
122 def lookup_interval(self, wire, start_ts, end_ts):
123 repo = self._factory.repo(wire)
124 fsobj = svn.repos.fs(repo)
125 start_rev = None
126 end_rev = None
127 if start_ts:
128 start_ts_svn = apr_time_t(start_ts)
129 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
130 else:
131 start_rev = 1
132 if end_ts:
133 end_ts_svn = apr_time_t(end_ts)
134 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
135 else:
136 end_rev = svn.fs.youngest_rev(fsobj)
137 return start_rev, end_rev
138
139 def revision_properties(self, wire, revision):
140 repo = self._factory.repo(wire)
141 fs_ptr = svn.repos.fs(repo)
142 return svn.fs.revision_proplist(fs_ptr, revision)
143
144 def revision_changes(self, wire, revision):
145
146 repo = self._factory.repo(wire)
147 fsobj = svn.repos.fs(repo)
148 rev_root = svn.fs.revision_root(fsobj, revision)
149
150 editor = svn.repos.ChangeCollector(fsobj, rev_root)
151 editor_ptr, editor_baton = svn.delta.make_editor(editor)
152 base_dir = ""
153 send_deltas = False
154 svn.repos.replay2(
155 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
156 editor_ptr, editor_baton, None)
157
158 added = []
159 changed = []
160 removed = []
161
162 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
163 for path, change in editor.changes.iteritems():
164 # TODO: Decide what to do with directory nodes. Subversion can add
165 # empty directories.
166 if change.item_kind == svn.core.svn_node_dir:
167 continue
168 if change.action == svn.repos.CHANGE_ACTION_ADD:
169 added.append(path)
170 elif change.action == svn.repos.CHANGE_ACTION_MODIFY:
171 changed.append(path)
172 elif change.action == svn.repos.CHANGE_ACTION_DELETE:
173 removed.append(path)
174 else:
175 raise NotImplementedError(
176 "Action %s not supported on path %s" % (
177 change.action, path))
178
179 changes = {
180 'added': added,
181 'changed': changed,
182 'removed': removed,
183 }
184 return changes
185
186 def node_history(self, wire, path, revision, limit):
187 cross_copies = False
188 repo = self._factory.repo(wire)
189 fsobj = svn.repos.fs(repo)
190 rev_root = svn.fs.revision_root(fsobj, revision)
191
192 history_revisions = []
193 history = svn.fs.node_history(rev_root, path)
194 history = svn.fs.history_prev(history, cross_copies)
195 while history:
196 __, node_revision = svn.fs.history_location(history)
197 history_revisions.append(node_revision)
198 if limit and len(history_revisions) >= limit:
199 break
200 history = svn.fs.history_prev(history, cross_copies)
201 return history_revisions
202
203 def node_properties(self, wire, path, revision):
204 repo = self._factory.repo(wire)
205 fsobj = svn.repos.fs(repo)
206 rev_root = svn.fs.revision_root(fsobj, revision)
207 return svn.fs.node_proplist(rev_root, path)
208
209 def file_annotate(self, wire, path, revision):
210 abs_path = 'file://' + urllib.pathname2url(
211 vcspath.join(wire['path'], path))
212 file_uri = svn.core.svn_path_canonicalize(abs_path)
213
214 start_rev = svn_opt_revision_value_t(0)
215 peg_rev = svn_opt_revision_value_t(revision)
216 end_rev = peg_rev
217
218 annotations = []
219
220 def receiver(line_no, revision, author, date, line, pool):
221 annotations.append((line_no, revision, line))
222
223 # TODO: Cannot use blame5, missing typemap function in the swig code
224 try:
225 svn.client.blame2(
226 file_uri, peg_rev, start_rev, end_rev,
227 receiver, svn.client.create_context())
228 except svn.core.SubversionException as exc:
229 log.exception("Error during blame operation.")
230 raise Exception(
231 "Blame not supported or file does not exist at path %s. "
232 "Error %s." % (path, exc))
233
234 return annotations
235
236 def get_node_type(self, wire, path, rev=None):
237 repo = self._factory.repo(wire)
238 fs_ptr = svn.repos.fs(repo)
239 if rev is None:
240 rev = svn.fs.youngest_rev(fs_ptr)
241 root = svn.fs.revision_root(fs_ptr, rev)
242 node = svn.fs.check_path(root, path)
243 return NODE_TYPE_MAPPING.get(node, None)
244
245 def get_nodes(self, wire, path, revision=None):
246 repo = self._factory.repo(wire)
247 fsobj = svn.repos.fs(repo)
248 if revision is None:
249 revision = svn.fs.youngest_rev(fsobj)
250 root = svn.fs.revision_root(fsobj, revision)
251 entries = svn.fs.dir_entries(root, path)
252 result = []
253 for entry_path, entry_info in entries.iteritems():
254 result.append(
255 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
256 return result
257
258 def get_file_content(self, wire, path, rev=None):
259 repo = self._factory.repo(wire)
260 fsobj = svn.repos.fs(repo)
261 if rev is None:
262 rev = svn.fs.youngest_revision(fsobj)
263 root = svn.fs.revision_root(fsobj, rev)
264 content = svn.core.Stream(svn.fs.file_contents(root, path))
265 return content.read()
266
267 def get_file_size(self, wire, path, revision=None):
268 repo = self._factory.repo(wire)
269 fsobj = svn.repos.fs(repo)
270 if revision is None:
271 revision = svn.fs.youngest_revision(fsobj)
272 root = svn.fs.revision_root(fsobj, revision)
273 size = svn.fs.file_length(root, path)
274 return size
275
276 def create_repository(self, wire, compatible_version=None):
277 log.info('Creating Subversion repository in path "%s"', wire['path'])
278 self._factory.repo(wire, create=True,
279 compatible_version=compatible_version)
280
281 def import_remote_repository(self, wire, src_url):
282 repo_path = wire['path']
283 if not self.is_path_valid_repository(wire, repo_path):
284 raise Exception(
285 "Path %s is not a valid Subversion repository." % repo_path)
286 # TODO: johbo: URL checks ?
287 rdump = subprocess.Popen(
288 ['svnrdump', 'dump', '--non-interactive', src_url],
289 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
290 load = subprocess.Popen(
291 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
292
293 # TODO: johbo: This can be a very long operation, might be better
294 # to track some kind of status and provide an api to check if the
295 # import is done.
296 rdump.wait()
297 load.wait()
298
299 if rdump.returncode != 0:
300 errors = rdump.stderr.read()
301 log.error('svnrdump dump failed: statuscode %s: message: %s',
302 rdump.returncode, errors)
303 reason = 'UNKNOWN'
304 if 'svnrdump: E230001:' in errors:
305 reason = 'INVALID_CERTIFICATE'
306 raise Exception(
307 'Failed to dump the remote repository from %s.' % src_url,
308 reason)
309 if load.returncode != 0:
310 raise Exception(
311 'Failed to load the dump of remote repository from %s.' %
312 (src_url, ))
313
314 def commit(self, wire, message, author, timestamp, updated, removed):
315 assert isinstance(message, str)
316 assert isinstance(author, str)
317
318 repo = self._factory.repo(wire)
319 fsobj = svn.repos.fs(repo)
320
321 rev = svn.fs.youngest_rev(fsobj)
322 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
323 txn_root = svn.fs.txn_root(txn)
324
325 for node in updated:
326 TxnNodeProcessor(node, txn_root).update()
327 for node in removed:
328 TxnNodeProcessor(node, txn_root).remove()
329
330 commit_id = svn.repos.fs_commit_txn(repo, txn)
331
332 if timestamp:
333 apr_time = apr_time_t(timestamp)
334 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
335 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
336
337 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
338 return commit_id
339
340 def diff(self, wire, rev1, rev2, path1=None, path2=None,
341 ignore_whitespace=False, context=3):
342 wire.update(cache=False)
343 repo = self._factory.repo(wire)
344 diff_creator = SvnDiffer(
345 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
346 return diff_creator.generate_diff()
347
348
349 class SvnDiffer(object):
350 """
351 Utility to create diffs based on difflib and the Subversion api
352 """
353
354 binary_content = False
355
356 def __init__(
357 self, repo, src_rev, src_path, tgt_rev, tgt_path,
358 ignore_whitespace, context):
359 self.repo = repo
360 self.ignore_whitespace = ignore_whitespace
361 self.context = context
362
363 fsobj = svn.repos.fs(repo)
364
365 self.tgt_rev = tgt_rev
366 self.tgt_path = tgt_path or ''
367 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
368 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
369
370 self.src_rev = src_rev
371 self.src_path = src_path or self.tgt_path
372 self.src_root = svn.fs.revision_root(fsobj, src_rev)
373 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
374
375 self._validate()
376
377 def _validate(self):
378 if (self.tgt_kind != svn.core.svn_node_none and
379 self.src_kind != svn.core.svn_node_none and
380 self.src_kind != self.tgt_kind):
381 # TODO: johbo: proper error handling
382 raise Exception(
383 "Source and target are not compatible for diff generation. "
384 "Source type: %s, target type: %s" %
385 (self.src_kind, self.tgt_kind))
386
387 def generate_diff(self):
388 buf = StringIO.StringIO()
389 if self.tgt_kind == svn.core.svn_node_dir:
390 self._generate_dir_diff(buf)
391 else:
392 self._generate_file_diff(buf)
393 return buf.getvalue()
394
395 def _generate_dir_diff(self, buf):
396 editor = DiffChangeEditor()
397 editor_ptr, editor_baton = svn.delta.make_editor(editor)
398 svn.repos.dir_delta2(
399 self.src_root,
400 self.src_path,
401 '', # src_entry
402 self.tgt_root,
403 self.tgt_path,
404 editor_ptr, editor_baton,
405 authorization_callback_allow_all,
406 False, # text_deltas
407 svn.core.svn_depth_infinity, # depth
408 False, # entry_props
409 False, # ignore_ancestry
410 )
411
412 for path, __, change in sorted(editor.changes):
413 self._generate_node_diff(
414 buf, change, path, self.tgt_path, path, self.src_path)
415
416 def _generate_file_diff(self, buf):
417 change = None
418 if self.src_kind == svn.core.svn_node_none:
419 change = "add"
420 elif self.tgt_kind == svn.core.svn_node_none:
421 change = "delete"
422 tgt_base, tgt_path = vcspath.split(self.tgt_path)
423 src_base, src_path = vcspath.split(self.src_path)
424 self._generate_node_diff(
425 buf, change, tgt_path, tgt_base, src_path, src_base)
426
427 def _generate_node_diff(
428 self, buf, change, tgt_path, tgt_base, src_path, src_base):
429 tgt_full_path = vcspath.join(tgt_base, tgt_path)
430 src_full_path = vcspath.join(src_base, src_path)
431
432 self.binary_content = False
433 mime_type = self._get_mime_type(tgt_full_path)
434 if mime_type and not mime_type.startswith('text'):
435 self.binary_content = True
436 buf.write("=" * 67 + '\n')
437 buf.write("Cannot display: file marked as a binary type.\n")
438 buf.write("svn:mime-type = %s\n" % mime_type)
439 buf.write("Index: %s\n" % (tgt_path, ))
440 buf.write("=" * 67 + '\n')
441 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
442 'tgt_path': tgt_path})
443
444 if change == 'add':
445 # TODO: johbo: SVN is missing a zero here compared to git
446 buf.write("new file mode 10644\n")
447 buf.write("--- /dev/null\t(revision 0)\n")
448 src_lines = []
449 else:
450 if change == 'delete':
451 buf.write("deleted file mode 10644\n")
452 buf.write("--- a/%s\t(revision %s)\n" % (
453 src_path, self.src_rev))
454 src_lines = self._svn_readlines(self.src_root, src_full_path)
455
456 if change == 'delete':
457 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
458 tgt_lines = []
459 else:
460 buf.write("+++ b/%s\t(revision %s)\n" % (
461 tgt_path, self.tgt_rev))
462 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
463
464 if not self.binary_content:
465 udiff = svn_diff.unified_diff(
466 src_lines, tgt_lines, context=self.context,
467 ignore_blank_lines=self.ignore_whitespace,
468 ignore_case=False,
469 ignore_space_changes=self.ignore_whitespace)
470 buf.writelines(udiff)
471
472 def _get_mime_type(self, path):
473 try:
474 mime_type = svn.fs.node_prop(
475 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
476 except svn.core.SubversionException:
477 mime_type = svn.fs.node_prop(
478 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
479 return mime_type
480
481 def _svn_readlines(self, fs_root, node_path):
482 if self.binary_content:
483 return []
484 node_kind = svn.fs.check_path(fs_root, node_path)
485 if node_kind not in (
486 svn.core.svn_node_file, svn.core.svn_node_symlink):
487 return []
488 content = svn.core.Stream(
489 svn.fs.file_contents(fs_root, node_path)).read()
490 return content.splitlines(True)
491
492
493 class DiffChangeEditor(svn.delta.Editor):
494 """
495 Records changes between two given revisions
496 """
497
498 def __init__(self):
499 self.changes = []
500
501 def delete_entry(self, path, revision, parent_baton, pool=None):
502 self.changes.append((path, None, 'delete'))
503
504 def add_file(
505 self, path, parent_baton, copyfrom_path, copyfrom_revision,
506 file_pool=None):
507 self.changes.append((path, 'file', 'add'))
508
509 def open_file(self, path, parent_baton, base_revision, file_pool=None):
510 self.changes.append((path, 'file', 'change'))
511
512
513 def authorization_callback_allow_all(root, path, pool):
514 return True
515
516
517 class TxnNodeProcessor(object):
518 """
519 Utility to process the change of one node within a transaction root.
520
521 It encapsulates the knowledge of how to add, update or remove
522 a node for a given transaction root. The purpose is to support the method
523 `SvnRemote.commit`.
524 """
525
526 def __init__(self, node, txn_root):
527 assert isinstance(node['path'], str)
528
529 self.node = node
530 self.txn_root = txn_root
531
532 def update(self):
533 self._ensure_parent_dirs()
534 self._add_file_if_node_does_not_exist()
535 self._update_file_content()
536 self._update_file_properties()
537
538 def remove(self):
539 svn.fs.delete(self.txn_root, self.node['path'])
540 # TODO: Clean up directory if empty
541
542 def _ensure_parent_dirs(self):
543 curdir = vcspath.dirname(self.node['path'])
544 dirs_to_create = []
545 while not self._svn_path_exists(curdir):
546 dirs_to_create.append(curdir)
547 curdir = vcspath.dirname(curdir)
548
549 for curdir in reversed(dirs_to_create):
550 log.debug('Creating missing directory "%s"', curdir)
551 svn.fs.make_dir(self.txn_root, curdir)
552
553 def _svn_path_exists(self, path):
554 path_status = svn.fs.check_path(self.txn_root, path)
555 return path_status != svn.core.svn_node_none
556
557 def _add_file_if_node_does_not_exist(self):
558 kind = svn.fs.check_path(self.txn_root, self.node['path'])
559 if kind == svn.core.svn_node_none:
560 svn.fs.make_file(self.txn_root, self.node['path'])
561
562 def _update_file_content(self):
563 assert isinstance(self.node['content'], str)
564 handler, baton = svn.fs.apply_textdelta(
565 self.txn_root, self.node['path'], None, None)
566 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
567
568 def _update_file_properties(self):
569 properties = self.node.get('properties', {})
570 for key, value in properties.iteritems():
571 svn.fs.change_node_prop(
572 self.txn_root, self.node['path'], key, value)
573
574
575 def apr_time_t(timestamp):
576 """
577 Convert a Python timestamp into APR timestamp type apr_time_t
578 """
579 return timestamp * 1E6
580
581
582 def svn_opt_revision_value_t(num):
583 """
584 Put `num` into a `svn_opt_revision_value_t` structure.
585 """
586 value = svn.core.svn_opt_revision_value_t()
587 value.number = num
588 revision = svn.core.svn_opt_revision_t()
589 revision.kind = svn.core.svn_opt_revision_number
590 revision.value = value
591 return revision
@@ -0,0 +1,207 b''
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2004-2009 Edgewall Software
4 # Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
5 # All rights reserved.
6 #
7 # This software is licensed as described in the file COPYING, which
8 # you should have received as part of this distribution. The terms
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
10 #
11 # This software consists of voluntary contributions made by many
12 # individuals. For the exact contribution history, see the revision
13 # history and logs, available at http://trac.edgewall.org/log/.
14 #
15 # Author: Christopher Lenz <cmlenz@gmx.de>
16
17 import difflib
18
19
20 def get_filtered_hunks(fromlines, tolines, context=None,
21 ignore_blank_lines=False, ignore_case=False,
22 ignore_space_changes=False):
23 """Retrieve differences in the form of `difflib.SequenceMatcher`
24 opcodes, grouped according to the ``context`` and ``ignore_*``
25 parameters.
26
27 :param fromlines: list of lines corresponding to the old content
28 :param tolines: list of lines corresponding to the new content
29 :param ignore_blank_lines: differences about empty lines only are ignored
30 :param ignore_case: upper case / lower case only differences are ignored
31 :param ignore_space_changes: differences in amount of spaces are ignored
32 :param context: the number of "equal" lines kept for representing
33 the context of the change
34 :return: generator of grouped `difflib.SequenceMatcher` opcodes
35
36 If none of the ``ignore_*`` parameters is `True`, there's nothing
37 to filter out the results will come straight from the
38 SequenceMatcher.
39 """
40 hunks = get_hunks(fromlines, tolines, context)
41 if ignore_space_changes or ignore_case or ignore_blank_lines:
42 hunks = filter_ignorable_lines(hunks, fromlines, tolines, context,
43 ignore_blank_lines, ignore_case,
44 ignore_space_changes)
45 return hunks
46
47
48 def get_hunks(fromlines, tolines, context=None):
49 """Generator yielding grouped opcodes describing differences .
50
51 See `get_filtered_hunks` for the parameter descriptions.
52 """
53 matcher = difflib.SequenceMatcher(None, fromlines, tolines)
54 if context is None:
55 return (hunk for hunk in [matcher.get_opcodes()])
56 else:
57 return matcher.get_grouped_opcodes(context)
58
59
60 def filter_ignorable_lines(hunks, fromlines, tolines, context,
61 ignore_blank_lines, ignore_case,
62 ignore_space_changes):
63 """Detect line changes that should be ignored and emits them as
64 tagged as "equal", possibly joined with the preceding and/or
65 following "equal" block.
66
67 See `get_filtered_hunks` for the parameter descriptions.
68 """
69 def is_ignorable(tag, fromlines, tolines):
70 if tag == 'delete' and ignore_blank_lines:
71 if ''.join(fromlines) == '':
72 return True
73 elif tag == 'insert' and ignore_blank_lines:
74 if ''.join(tolines) == '':
75 return True
76 elif tag == 'replace' and (ignore_case or ignore_space_changes):
77 if len(fromlines) != len(tolines):
78 return False
79 def f(str):
80 if ignore_case:
81 str = str.lower()
82 if ignore_space_changes:
83 str = ' '.join(str.split())
84 return str
85 for i in range(len(fromlines)):
86 if f(fromlines[i]) != f(tolines[i]):
87 return False
88 return True
89
90 hunks = list(hunks)
91 opcodes = []
92 ignored_lines = False
93 prev = None
94 for hunk in hunks:
95 for tag, i1, i2, j1, j2 in hunk:
96 if tag == 'equal':
97 if prev:
98 prev = (tag, prev[1], i2, prev[3], j2)
99 else:
100 prev = (tag, i1, i2, j1, j2)
101 else:
102 if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]):
103 ignored_lines = True
104 if prev:
105 prev = 'equal', prev[1], i2, prev[3], j2
106 else:
107 prev = 'equal', i1, i2, j1, j2
108 continue
109 if prev:
110 opcodes.append(prev)
111 opcodes.append((tag, i1, i2, j1, j2))
112 prev = None
113 if prev:
114 opcodes.append(prev)
115
116 if ignored_lines:
117 if context is None:
118 yield opcodes
119 else:
120 # we leave at most n lines with the tag 'equal' before and after
121 # every change
122 n = context
123 nn = n + n
124
125 group = []
126 def all_equal():
127 all(op[0] == 'equal' for op in group)
128 for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
129 if idx == 0 and tag == 'equal': # Fixup leading unchanged block
130 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
131 elif tag == 'equal' and i2 - i1 > nn:
132 group.append((tag, i1, min(i2, i1 + n), j1,
133 min(j2, j1 + n)))
134 if not all_equal():
135 yield group
136 group = []
137 i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
138 group.append((tag, i1, i2, j1, j2))
139
140 if group and not (len(group) == 1 and group[0][0] == 'equal'):
141 if group[-1][0] == 'equal': # Fixup trailing unchanged block
142 tag, i1, i2, j1, j2 = group[-1]
143 group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)
144 if not all_equal():
145 yield group
146 else:
147 for hunk in hunks:
148 yield hunk
149
150
151 NO_NEWLINE_AT_END = '\\ No newline at end of file'
152
153
154 def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0,
155 ignore_case=0, ignore_space_changes=0, lineterm='\n'):
156 """
157 Generator producing lines corresponding to a textual diff.
158
159 See `get_filtered_hunks` for the parameter descriptions.
160 """
161 # TODO: johbo: Check if this can be nicely integrated into the matching
162 if ignore_space_changes:
163 fromlines = [l.strip() for l in fromlines]
164 tolines = [l.strip() for l in tolines]
165
166 for group in get_filtered_hunks(fromlines, tolines, context,
167 ignore_blank_lines, ignore_case,
168 ignore_space_changes):
169 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
170 if i1 == 0 and i2 == 0:
171 i1, i2 = -1, -1 # support for Add changes
172 if j1 == 0 and j2 == 0:
173 j1, j2 = -1, -1 # support for Delete changes
174 yield '@@ -%s +%s @@%s' % (
175 _hunk_range(i1 + 1, i2 - i1),
176 _hunk_range(j1 + 1, j2 - j1),
177 lineterm)
178 for tag, i1, i2, j1, j2 in group:
179 if tag == 'equal':
180 for line in fromlines[i1:i2]:
181 if not line.endswith(lineterm):
182 yield ' ' + line + lineterm
183 yield NO_NEWLINE_AT_END + lineterm
184 else:
185 yield ' ' + line
186 else:
187 if tag in ('replace', 'delete'):
188 for line in fromlines[i1:i2]:
189 if not line.endswith(lineterm):
190 yield '-' + line + lineterm
191 yield NO_NEWLINE_AT_END + lineterm
192 else:
193 yield '-' + line
194 if tag in ('replace', 'insert'):
195 for line in tolines[j1:j2]:
196 if not line.endswith(lineterm):
197 yield '+' + line + lineterm
198 yield NO_NEWLINE_AT_END + lineterm
199 else:
200 yield '+' + line
201
202
203 def _hunk_range(start, length):
204 if length != 1:
205 return '%d,%d' % (start, length)
206 else:
207 return '%d' % (start, )
@@ -0,0 +1,57 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18
19
20 # TODO: johbo: That's a copy from rhodecode
21 def safe_str(unicode_, to_encoding=['utf8']):
22 """
23 safe str function. Does few trick to turn unicode_ into string
24
25 In case of UnicodeEncodeError, we try to return it with encoding detected
26 by chardet library if it fails fallback to string with errors replaced
27
28 :param unicode_: unicode to encode
29 :rtype: str
30 :returns: str object
31 """
32
33 # if it's not basestr cast to str
34 if not isinstance(unicode_, basestring):
35 return str(unicode_)
36
37 if isinstance(unicode_, str):
38 return unicode_
39
40 if not isinstance(to_encoding, (list, tuple)):
41 to_encoding = [to_encoding]
42
43 for enc in to_encoding:
44 try:
45 return unicode_.encode(enc)
46 except UnicodeEncodeError:
47 pass
48
49 try:
50 import chardet
51 encoding = chardet.detect(unicode_)['encoding']
52 if encoding is None:
53 raise UnicodeEncodeError()
54
55 return unicode_.encode(encoding)
56 except (ImportError, UnicodeEncodeError):
57 return unicode_.encode(to_encoding[0], 'replace')
@@ -0,0 +1,116 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 """Extract the responses of a WSGI app."""
19
20 __all__ = ('WSGIAppCaller',)
21
22 import io
23 import logging
24 import os
25
26
27 log = logging.getLogger(__name__)
28
29 DEV_NULL = open(os.devnull)
30
31
32 def _complete_environ(environ, input_data):
33 """Update the missing wsgi.* variables of a WSGI environment.
34
35 :param environ: WSGI environment to update
36 :type environ: dict
37 :param input_data: data to be read by the app
38 :type input_data: str
39 """
40 environ.update({
41 'wsgi.version': (1, 0),
42 'wsgi.url_scheme': 'http',
43 'wsgi.multithread': True,
44 'wsgi.multiprocess': True,
45 'wsgi.run_once': False,
46 'wsgi.input': io.BytesIO(input_data),
47 'wsgi.errors': DEV_NULL,
48 })
49
50
51 # pylint: disable=too-few-public-methods
52 class _StartResponse(object):
53 """Save the arguments of a start_response call."""
54
55 __slots__ = ['status', 'headers', 'content']
56
57 def __init__(self):
58 self.status = None
59 self.headers = None
60 self.content = []
61
62 def __call__(self, status, headers, exc_info=None):
63 # TODO(skreft): do something meaningful with the exc_info
64 exc_info = None # avoid dangling circular reference
65 self.status = status
66 self.headers = headers
67
68 return self.write
69
70 def write(self, content):
71 """Write method returning when calling this object.
72
73 All the data written is then available in content.
74 """
75 self.content.append(content)
76
77
78 class WSGIAppCaller(object):
79 """Calls a WSGI app."""
80
81 def __init__(self, app):
82 """
83 :param app: WSGI app to call
84 """
85 self.app = app
86
87 def handle(self, environ, input_data):
88 """Process a request with the WSGI app.
89
90 The returned data of the app is fully consumed into a list.
91
92 :param environ: WSGI environment to update
93 :type environ: dict
94 :param input_data: data to be read by the app
95 :type input_data: str
96
97 :returns: a tuple with the contents, status and headers
98 :rtype: (list<str>, str, list<(str, str)>)
99 """
100 _complete_environ(environ, input_data)
101 start_response = _StartResponse()
102 log.debug("Calling wrapped WSGI application")
103 responses = self.app(environ, start_response)
104 responses_list = list(responses)
105 existing_responses = start_response.content
106 if existing_responses:
107 log.debug(
108 "Adding returned response to response written via write()")
109 existing_responses.extend(responses_list)
110 responses_list = existing_responses
111 if hasattr(responses, 'close'):
112 log.debug("Closing iterator from WSGI application")
113 responses.close()
114
115 log.debug("Handling of WSGI request done, returning response")
116 return responses_list, start_response.status, start_response.headers
General Comments 0
You need to be logged in to leave comments. Login now