Imported Upstream version 2.3.4
340
LICENSE
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Library General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Library General
|
||||||
|
Public License instead of this License.
|
10
MANIFEST.in
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
include LICENSE README.rst requirements.txt UPGRADING.rst
|
||||||
|
include createdb.py
|
||||||
|
recursive-include pagure *
|
||||||
|
recursive-include files *
|
||||||
|
recursive-include milters *
|
||||||
|
recursive-include tests *
|
||||||
|
recursive-include doc *
|
||||||
|
recursive-include alembic *
|
||||||
|
recursive-include ev-server *
|
||||||
|
recursive-include webhook-server *
|
11
PKG-INFO
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Metadata-Version: 1.1
|
||||||
|
Name: pagure
|
||||||
|
Version: 2.3.4
|
||||||
|
Summary: A light-weight git-centered forge based on pygit2..
|
||||||
|
Home-page: https://fedorahosted.org/pagure/
|
||||||
|
Author: Pierre-Yves Chibon
|
||||||
|
Author-email: pingou@pingoured.fr
|
||||||
|
License: GPLv2+
|
||||||
|
Download-URL: https://fedorahosted.org/releases/p/r/pagure/
|
||||||
|
Description: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
81
README.rst
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
Pagure
|
||||||
|
======
|
||||||
|
|
||||||
|
:Author: Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
|
||||||
|
Pagure is a git-centered forge, python based using pygit2.
|
||||||
|
|
||||||
|
With pagure you can host your project with its documentation, let your users
|
||||||
|
report issues or request enhancements using the ticketing system and build your
|
||||||
|
community of contributors by allowing them to fork your projects and contribute
|
||||||
|
to it via the now-popular pull-request mechanism.
|
||||||
|
|
||||||
|
|
||||||
|
Homepage: https://pagure.io/pagure
|
||||||
|
|
||||||
|
See it at work: https://pagure.io
|
||||||
|
|
||||||
|
|
||||||
|
Playground version: https://stg.pagure.io
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Get it running
|
||||||
|
==============
|
||||||
|
|
||||||
|
* Install the needed system libraries::
|
||||||
|
|
||||||
|
sudo dnf install git python-virtualenv libgit2-devel \
|
||||||
|
libjpeg-devel gcc libffi-devel redhat-rpm-config
|
||||||
|
|
||||||
|
.. note:: Do note the version of libgit2 that you install, for example
|
||||||
|
in ``libgit2-0.23.4-1`` you need to keep in mind the ``0.23``
|
||||||
|
|
||||||
|
* Retrieve the sources::
|
||||||
|
|
||||||
|
git clone https://pagure.io/pagure.git
|
||||||
|
cd pagure
|
||||||
|
|
||||||
|
* Install dependencies
|
||||||
|
|
||||||
|
* create the virtualenv::
|
||||||
|
|
||||||
|
virtualenv pagure_env
|
||||||
|
source ./pagure_env/bin/activate
|
||||||
|
|
||||||
|
* Install the correct version of pygit2::
|
||||||
|
|
||||||
|
pip install pygit2==<version of libgit2 found>.*
|
||||||
|
|
||||||
|
So in our example::
|
||||||
|
|
||||||
|
pip install pygit2==0.23.*
|
||||||
|
|
||||||
|
* Install the rest of the dependencies::
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
* Create the folder that will receive the projects, forks, docs, requests and
|
||||||
|
tickets' git repo::
|
||||||
|
|
||||||
|
mkdir {repos,docs,forks,tickets,requests}
|
||||||
|
|
||||||
|
|
||||||
|
* Create the inital database scheme::
|
||||||
|
|
||||||
|
python createdb.py
|
||||||
|
|
||||||
|
|
||||||
|
* Run it::
|
||||||
|
|
||||||
|
./runserver.py
|
||||||
|
|
||||||
|
|
||||||
|
* To get some profiling information you can also run it as::
|
||||||
|
|
||||||
|
./runserver.py --profile
|
||||||
|
|
||||||
|
|
||||||
|
This will launch the application at http://127.0.0.1:5000
|
187
UPGRADING.rst
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
Upgrading Pagure
|
||||||
|
================
|
||||||
|
|
||||||
|
|
||||||
|
2.3.4
|
||||||
|
-----
|
||||||
|
|
||||||
|
Release 2.3.4 contains an important security fix, blocking a source of XSS
|
||||||
|
attack. (CVE-2016-1000037)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
From 2.2 to 2.3
|
||||||
|
---------------
|
||||||
|
|
||||||
|
2.3 brings a few changes impacting the database scheme, including a new
|
||||||
|
`duplicate` status for tickets, a feature allowing one to `watch` or
|
||||||
|
`unwatch` a project and notifications on tickets as exist on pull-requests.
|
||||||
|
|
||||||
|
Therefore, when upgrading from 2.2.x to 2.3, you will have to :
|
||||||
|
|
||||||
|
* Create the new DB tables and the new status field using the ``createdb.py`` script.
|
||||||
|
|
||||||
|
* Update the database schame using alembic: ``alembic upgrade head``
|
||||||
|
|
||||||
|
This update also brings a new configuration key:
|
||||||
|
|
||||||
|
* ``PAGURE_ADMIN_USERS`` allows to mark some users as instance-wide admins, giving
|
||||||
|
them full access to every projects, private or not. This feature can then be
|
||||||
|
used as a way to clean spams.
|
||||||
|
* ``SMTP_PORT`` allows to specify the port to use when contacting the SMTP
|
||||||
|
server
|
||||||
|
* ``SMTP_SSL`` allows to specify whether to use SSL when contacting the SMTP
|
||||||
|
server
|
||||||
|
* ``SMTP_USERNAME`` and ``SMTP_PASSWORD`` if provided together allow to contact
|
||||||
|
an SMTP requiring authentication.
|
||||||
|
|
||||||
|
In this update is also added the script ``api_key_expire_mail.py`` meant to be
|
||||||
|
run by a daily cron job and warning users when their API token is nearing its
|
||||||
|
expiration date.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.2.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
Release 2.2.2 contains an important security fix, blocking a source of XSS
|
||||||
|
attack.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
From 2.1 to 2.2
|
||||||
|
---------------
|
||||||
|
|
||||||
|
2.2 brings a number of bug fixes and a few improvements.
|
||||||
|
|
||||||
|
One of the major changes impacts the databases where we must change some of the
|
||||||
|
table so that the foreign key cascade on delete (fixes deleting a project when a
|
||||||
|
few plugins were activated).
|
||||||
|
|
||||||
|
When upgrading for 2.1 to 2.2 all you will have to do is:
|
||||||
|
|
||||||
|
* Update the database scheme using alembic: ``alembic upgrade head``
|
||||||
|
|
||||||
|
.. note:: If you run another database system than PostgreSQL the alembic
|
||||||
|
revision ``317a285e04a8_delete_hooks.py`` will require adjustment as the
|
||||||
|
foreign key constraints are named and the names are driver dependant.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
From 2.0 to 2.1
|
||||||
|
---------------
|
||||||
|
|
||||||
|
2.1 brings its usual flow of improvements and bug fixes.
|
||||||
|
|
||||||
|
When upgrading from 2.0.x to 2.1 all you will have to:
|
||||||
|
|
||||||
|
* Update the database schame using alembic: ``alembic upgrade head``
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
From 1.x to 2.0
|
||||||
|
---------------
|
||||||
|
|
||||||
|
As the version change indicates, 2.0 brings quite a number of changes,
|
||||||
|
including some that are not backward compatible.
|
||||||
|
|
||||||
|
When upgrading to 2.0 you will have to:
|
||||||
|
|
||||||
|
* Update the database schema using alembic: ``alembic upgrade head``
|
||||||
|
|
||||||
|
* Create the new DB tables so that the new plugins work using the
|
||||||
|
``createdb.py`` script
|
||||||
|
|
||||||
|
* Move the forks git repo
|
||||||
|
|
||||||
|
Forked git repos are now located under the same folder as the regular git
|
||||||
|
repos, just under a ``forks/`` subfolder.
|
||||||
|
So the structure changes from: ::
|
||||||
|
|
||||||
|
repos/
|
||||||
|
├── foo.git
|
||||||
|
└── bar.git
|
||||||
|
|
||||||
|
forks/
|
||||||
|
├── patrick/
|
||||||
|
│ ├── test.git
|
||||||
|
│ └── ipsilon.git
|
||||||
|
└── pingou/
|
||||||
|
├── foo.git
|
||||||
|
└── bar.git
|
||||||
|
|
||||||
|
to: ::
|
||||||
|
|
||||||
|
repos/
|
||||||
|
├── foo.git
|
||||||
|
├── bar.git
|
||||||
|
└── forks/
|
||||||
|
├── patrick/
|
||||||
|
│ ├── test.git
|
||||||
|
│ └── ipsilon.git
|
||||||
|
└── pingou/
|
||||||
|
├── foo.git
|
||||||
|
└── bar.git
|
||||||
|
|
||||||
|
So the entire ``forks`` folder is moved under the ``repos`` folder where
|
||||||
|
the other repositories are, containing the sources of the projects.
|
||||||
|
|
||||||
|
|
||||||
|
Git repos for ``tickets``, ``requests`` and ``docs`` will be trickier to
|
||||||
|
move as the structure changes from: ::
|
||||||
|
|
||||||
|
tickets/
|
||||||
|
├── foo.git
|
||||||
|
├── bar.git
|
||||||
|
├── patrick/
|
||||||
|
│ ├── test.git
|
||||||
|
│ └── ipsilon.git
|
||||||
|
└── pingou/
|
||||||
|
├── foo.git
|
||||||
|
└── bar.git
|
||||||
|
|
||||||
|
to: ::
|
||||||
|
|
||||||
|
tickets/
|
||||||
|
├── foo.git
|
||||||
|
├── bar.git
|
||||||
|
└── forks/
|
||||||
|
├── patrick/
|
||||||
|
│ ├── test.git
|
||||||
|
│ └── ipsilon.git
|
||||||
|
└── pingou/
|
||||||
|
├── foo.git
|
||||||
|
└── bar.git
|
||||||
|
|
||||||
|
Same for the ``requests`` and the ``docs`` git repos.
|
||||||
|
|
||||||
|
As you can see in the ``tickets``, ``requests`` and ``docs`` folders there
|
||||||
|
are two types of folders, git repos which are folder with a name ending
|
||||||
|
with ``.git``, and folder corresponding to usernames. These last ones are
|
||||||
|
the ones to be moved into a subfolder ``forks/``.
|
||||||
|
|
||||||
|
This can be done using something like: ::
|
||||||
|
|
||||||
|
mkdir forks
|
||||||
|
for i in `ls -1 |grep -v '\.git'`; do mv $i forks/; done
|
||||||
|
|
||||||
|
* Re-generate the gitolite configuration.
|
||||||
|
|
||||||
|
This can be done via the ``Re-generate gitolite ACLs file`` button in the
|
||||||
|
admin page.
|
||||||
|
|
||||||
|
* Keep URLs backward compatible
|
||||||
|
|
||||||
|
The support of pseudo-namespace in pagure 2.0 has required some changes
|
||||||
|
to the URL schema:
|
||||||
|
https://pagure.io/pagure/053d8cc95fcd50c23a8b0a7f70e55f8d1cc7aebb
|
||||||
|
became:
|
||||||
|
https://pagure.io/pagure/c/053d8cc95fcd50c23a8b0a7f70e55f8d1cc7aebb
|
||||||
|
(Note the added /c/ in it)
|
||||||
|
|
||||||
|
We introduced a backward compatibility fix for this.
|
||||||
|
|
||||||
|
This fix is however *disabled* by default so if you wish to keep the URLs
|
||||||
|
valid, you will need to adjust you configuration file to include: ::
|
||||||
|
|
||||||
|
OLD_VIEW_COMMIT_ENABLED = True
|
71
alembic/env.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = None
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(url=url, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
engine = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool)
|
||||||
|
|
||||||
|
connection = engine.connect()
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
|
|
22
alembic/script.py.mako
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
45
alembic/versions/15ea3c2cf83d_pr_comment_editing.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
"""Adding column to store edited_by and edited_on a PR comment
|
||||||
|
|
||||||
|
Revision ID: 15ea3c2cf83d
|
||||||
|
Revises: 1cd0a853c697
|
||||||
|
Create Date: 2015-11-09 16:18:47.192088
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '15ea3c2cf83d'
|
||||||
|
down_revision = '1cd0a853c697'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the columns editor_id and edited_on to the table
|
||||||
|
pull_request_comments.
|
||||||
|
'''
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'pull_request_comments',
|
||||||
|
sa.Column(
|
||||||
|
'editor_id',
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||||
|
nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'pull_request_comments',
|
||||||
|
sa.Column(
|
||||||
|
'edited_on',
|
||||||
|
sa.DateTime,
|
||||||
|
nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the columns editor_id and edited_on from the table
|
||||||
|
pull_request_comments.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_request_comments', 'editor_id')
|
||||||
|
op.drop_column('pull_request_comments', 'edited_on')
|
38
alembic/versions/1b6d7dc5600a_versioning_passwords.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
"""versioning_passwords
|
||||||
|
|
||||||
|
Revision ID: 1b6d7dc5600a
|
||||||
|
Revises: 3b441ef4e928
|
||||||
|
Create Date: 2016-01-13 07:57:23.465676
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1b6d7dc5600a'
|
||||||
|
down_revision = '3b441ef4e928'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy.orm
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pagure.lib import model
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '.')
|
||||||
|
from pagure.lib import model
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
engine = op.get_bind()
|
||||||
|
Session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker())
|
||||||
|
Session.configure(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
for user in session.query(model.User).filter(
|
||||||
|
model.User.password != None).all():
|
||||||
|
user.password = '$1$%s' % user.password
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
raise ValueError("Password can not be downgraded")
|
37
alembic/versions/1cd0a853c697_add_closed_at_field_in_pr.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""Add closed_at field in PR
|
||||||
|
|
||||||
|
|
||||||
|
Revision ID: 1cd0a853c697
|
||||||
|
Revises: 6190226bed0
|
||||||
|
Create Date: 2015-10-02 09:32:15.370676
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1cd0a853c697'
|
||||||
|
down_revision = '6190226bed0'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column closed_at to the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column(
|
||||||
|
'closed_at',
|
||||||
|
sa.DateTime,
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute('''UPDATE "pull_requests" SET closed_at=date_created '''
|
||||||
|
'''WHERE STATUS != 'Open';''')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column closed_at from the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_requests', 'closed_at')
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Add the tree_id column to PR inline comments
|
||||||
|
|
||||||
|
Revision ID: 1f3de3853a1a
|
||||||
|
Revises: 58e60d869326
|
||||||
|
Create Date: 2016-02-22 16:13:59.943083
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1f3de3853a1a'
|
||||||
|
down_revision = '58e60d869326'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column tree_id to the table pull_request_comments.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_request_comments',
|
||||||
|
sa.Column('tree_id', sa.String(40), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column tree_id from the table pull_request_comments.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_request_comments', 'tree_id')
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""Add notifications to tickets
|
||||||
|
|
||||||
|
Revision ID: 22db0a833d35
|
||||||
|
Revises: 317a285e04a8
|
||||||
|
Create Date: 2016-06-27 16:10:33.395495
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '22db0a833d35'
|
||||||
|
down_revision = '317a285e04a8'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column notification to the table issue_comments.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'issue_comments',
|
||||||
|
sa.Column('notification', sa.Boolean, default=False, nullable=True)
|
||||||
|
)
|
||||||
|
op.execute('''UPDATE "issue_comments" SET notification=False;''')
|
||||||
|
op.alter_column(
|
||||||
|
'issue_comments', 'notification',
|
||||||
|
nullable=False, existing_nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column notification from the table issue_comments.
|
||||||
|
'''
|
||||||
|
op.drop_column('issue_comments', 'notification')
|
46
alembic/versions/257a7ce22682_add_the_remote_git_entry.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""Add the remote_git entry
|
||||||
|
|
||||||
|
Revision ID: 257a7ce22682
|
||||||
|
Revises: 36116bb7a69b
|
||||||
|
Create Date: 2015-07-21 14:26:23.989220
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '257a7ce22682'
|
||||||
|
down_revision = '36116bb7a69b'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column remote_git to the table pull_requests and make the
|
||||||
|
project_id_from field nullable.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column('remote_git', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
'pull_requests',
|
||||||
|
column_name='project_id_from',
|
||||||
|
nullable=True,
|
||||||
|
existing_nullable=False)
|
||||||
|
op.create_check_constraint(
|
||||||
|
"ck_lcl_or_remo_pr",
|
||||||
|
"pull_requests",
|
||||||
|
'NOT(project_id_from IS NULL AND remote_git IS NULL)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column remote_git from the table pull_requests and make
|
||||||
|
the project_id_from field not nullable.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_requests', 'remote_git')
|
||||||
|
op.alter_column(
|
||||||
|
'pull_requests',
|
||||||
|
column_name='project_id_from',
|
||||||
|
nullable=False,
|
||||||
|
existing_nullable=True)
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""Change the status of pull_requests
|
||||||
|
|
||||||
|
|
||||||
|
Revision ID: 298891e63039
|
||||||
|
Revises: 3c25e14b855b
|
||||||
|
Create Date: 2015-06-08 13:06:11.938966
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '298891e63039'
|
||||||
|
down_revision = '3c25e14b855b'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Adjust the status column of the pull_requests table.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column(
|
||||||
|
'_status', sa.Text,
|
||||||
|
sa.ForeignKey(
|
||||||
|
'status_pull_requests.status', onupdate='CASCADE'),
|
||||||
|
default='Open',
|
||||||
|
nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute('''UPDATE "pull_requests" '''
|
||||||
|
'''SET _status='Open' WHERE status=TRUE;''')
|
||||||
|
op.execute('''UPDATE "pull_requests" '''
|
||||||
|
'''SET _status='Merged' WHERE status=FALSE;''')
|
||||||
|
|
||||||
|
op.drop_column('pull_requests', 'status')
|
||||||
|
op.alter_column(
|
||||||
|
'pull_requests',
|
||||||
|
column_name='_status', new_column_name='status',
|
||||||
|
nullable=False, existing_nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Revert the status column of the pull_requests table.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column(
|
||||||
|
'_status', sa.Boolean, default=True, nullable=True)
|
||||||
|
)
|
||||||
|
op.execute('''UPDATE "pull_requests" '''
|
||||||
|
'''SET _status=TRUE WHERE status='Open';''')
|
||||||
|
op.execute('''UPDATE "pull_requests" '''
|
||||||
|
'''SET _status=FALSE WHERE status!='Open';''')
|
||||||
|
|
||||||
|
op.drop_column('pull_requests', 'status')
|
||||||
|
op.alter_column(
|
||||||
|
'pull_requests',
|
||||||
|
column_name='_status', new_column_name='status',
|
||||||
|
nullable=False, existing_nullable=True)
|
31
alembic/versions/2aa7b3958bc5_add_the_milestones_column.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""Add the milestones column
|
||||||
|
|
||||||
|
Revision ID: 2aa7b3958bc5
|
||||||
|
Revises: 443e090da188
|
||||||
|
Create Date: 2016-05-03 15:59:04.992414
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2aa7b3958bc5'
|
||||||
|
down_revision = '443e090da188'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column _milestones to the table projects
|
||||||
|
and the column milestone to the table issues.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'projects',
|
||||||
|
sa.Column('_milestones', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Drop the column _milestones from the table projects
|
||||||
|
and the column milestone from the table issues.
|
||||||
|
'''
|
||||||
|
op.drop_column('projects', '_milestones')
|
59
alembic/versions/317a285e04a8_delete_hooks.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
"""Delete hooks
|
||||||
|
|
||||||
|
Revision ID: 317a285e04a8
|
||||||
|
Revises: 2aa7b3958bc5
|
||||||
|
Create Date: 2016-05-30 11:28:48.512577
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '317a285e04a8'
|
||||||
|
down_revision = '2aa7b3958bc5'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
""" Alter the hooks table to update the foreign key to cascade on delete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for table in [
|
||||||
|
'hook_fedmsg', 'hook_irc', 'hook_mail',
|
||||||
|
'hook_pagure_force_commit', 'hook_pagure', 'hook_pagure_requests',
|
||||||
|
'hook_pagure_tickets', 'hook_pagure_unsigned_commit', 'hook_rtd',
|
||||||
|
]:
|
||||||
|
op.drop_constraint(
|
||||||
|
'%s_project_id_fkey' % table,
|
||||||
|
table,
|
||||||
|
type_='foreignkey')
|
||||||
|
op. create_foreign_key(
|
||||||
|
name='%s_project_id_fkey' % table,
|
||||||
|
source_table=table,
|
||||||
|
referent_table='projects',
|
||||||
|
local_cols=['project_id'],
|
||||||
|
remote_cols=['id'],
|
||||||
|
onupdate='cascade',
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_constraint(
|
||||||
|
'projects_groups_project_id_fkey',
|
||||||
|
'projects_groups',
|
||||||
|
type_='foreignkey')
|
||||||
|
op. create_foreign_key(
|
||||||
|
name='projects_groups_project_id_fkey',
|
||||||
|
source_table='projects_groups',
|
||||||
|
referent_table='projects',
|
||||||
|
local_cols=['project_id'],
|
||||||
|
remote_cols=['id'],
|
||||||
|
onupdate='cascade',
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
""" Alter the hooks table to update the foreign key to undo the cascade
|
||||||
|
on delete.
|
||||||
|
"""
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Add the url field to project
|
||||||
|
|
||||||
|
Revision ID: 36116bb7a69b
|
||||||
|
Revises: abc71fd60fa
|
||||||
|
Create Date: 2015-06-11 12:36:33.544046
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '36116bb7a69b'
|
||||||
|
down_revision = 'abc71fd60fa'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column url to the table projects.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'projects',
|
||||||
|
sa.Column('url', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column merge_status from the table projects.
|
||||||
|
'''
|
||||||
|
op.drop_column('projects', 'url')
|
44
alembic/versions/3b441ef4e928_comment_editing_issue.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
"""Adding column to store edited_by and edited_on a issue comment
|
||||||
|
|
||||||
|
Revision ID: 3b441ef4e928
|
||||||
|
Revises: 15ea3c2cf83d
|
||||||
|
Create Date: 2015-12-03 12:34:28.316699
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3b441ef4e928'
|
||||||
|
down_revision = '15ea3c2cf83d'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the columns editor_id and edited_on to the table issue_comments.
|
||||||
|
'''
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'issue_comments',
|
||||||
|
sa.Column(
|
||||||
|
'editor_id',
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||||
|
nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'issue_comments',
|
||||||
|
sa.Column(
|
||||||
|
'edited_on',
|
||||||
|
sa.DateTime,
|
||||||
|
nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the columns editor_id and edited_on from the table
|
||||||
|
issue_comments.
|
||||||
|
'''
|
||||||
|
op.drop_column('issue_comments', 'editor_id')
|
||||||
|
op.drop_column('issue_comments', 'edited_on')
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""add an avatar email for project
|
||||||
|
|
||||||
|
Revision ID: 3c25e14b855b
|
||||||
|
Revises: b5efae6bb23
|
||||||
|
Create Date: 2015-06-08 12:05:13.832348
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3c25e14b855b'
|
||||||
|
down_revision = 'b5efae6bb23'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column merge_status to the table projects.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'projects',
|
||||||
|
sa.Column('avatar_email', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column merge_status from the table projects.
|
||||||
|
'''
|
||||||
|
op.drop_column('projects', 'avatar_email')
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""add_closed_at_attribute_in_issues
|
||||||
|
|
||||||
|
Revision ID: 43df5e588a87
|
||||||
|
Revises: 22db0a833d35
|
||||||
|
Create Date: 2016-06-28 22:59:36.653905
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '43df5e588a87'
|
||||||
|
down_revision = '22db0a833d35'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add closed_at column in issues table '''
|
||||||
|
op.add_column(
|
||||||
|
'issues',
|
||||||
|
sa.Column('closed_at', sa.DateTime, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the closed_at column in issues table '''
|
||||||
|
op.drop_column('issues', 'closed_at')
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""up to 255 characters for project.name
|
||||||
|
|
||||||
|
Revision ID: 443e090da188
|
||||||
|
Revises: 496f7a700f2e
|
||||||
|
Create Date: 2016-04-20 17:57:36.385103
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '443e090da188'
|
||||||
|
down_revision = '496f7a700f2e'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.alter_column(
|
||||||
|
table_name='projects',
|
||||||
|
column_name='name',
|
||||||
|
type_=sa.String(255),
|
||||||
|
existing_type=sa.String(32)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.alter_column(
|
||||||
|
table_name='projects',
|
||||||
|
column_name='name',
|
||||||
|
type_=sa.String(32),
|
||||||
|
existing_type=sa.String(255)
|
||||||
|
)
|
37
alembic/versions/496f7a700f2e_add_priorities.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""Add priorities
|
||||||
|
|
||||||
|
Revision ID: 496f7a700f2e
|
||||||
|
Revises: 4cae55a80a42
|
||||||
|
Create Date: 2016-03-24 12:19:34.298752
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '496f7a700f2e'
|
||||||
|
down_revision = '4cae55a80a42'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column _priorities to the table projects
|
||||||
|
and the column priority to the table issues.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'projects',
|
||||||
|
sa.Column('_priorities', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'issues',
|
||||||
|
sa.Column('priority', sa.Integer, nullable=True, default=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Drop the column _priorities from the table projects
|
||||||
|
and the column priority from the table issues.
|
||||||
|
'''
|
||||||
|
op.drop_column('projects', '_priorities')
|
||||||
|
op.drop_column('issues', 'priority')
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Add the initial_comment on the PR table
|
||||||
|
|
||||||
|
Revision ID: 4cae55a80a42
|
||||||
|
Revises: 1f3de3853a1a
|
||||||
|
Create Date: 2016-03-01 12:00:34.823097
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4cae55a80a42'
|
||||||
|
down_revision = '1f3de3853a1a'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column initial_comment to the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column('initial_comment', sa.Text, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column initial_comment from the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_requests', 'initial_comment')
|
33
alembic/versions/58e60d869326_add_notification_bool_to_pr.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""add notification bool to PR
|
||||||
|
|
||||||
|
Revision ID: 58e60d869326
|
||||||
|
Revises: 1b6d7dc5600a
|
||||||
|
Create Date: 2016-02-12 12:39:07.839530
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '58e60d869326'
|
||||||
|
down_revision = '1b6d7dc5600a'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column notification to the table pull_request_comments.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_request_comments',
|
||||||
|
sa.Column('notification', sa.Boolean, default=False, nullable=True)
|
||||||
|
)
|
||||||
|
op.execute('''UPDATE "pull_request_comments" SET notification=False;''')
|
||||||
|
op.alter_column(
|
||||||
|
'pull_request_comments', 'notification',
|
||||||
|
nullable=False, existing_nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column notification from the table pull_request_comments.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_request_comments', 'notification')
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""Add the updated_on column to pull-requests
|
||||||
|
|
||||||
|
Revision ID: 6190226bed0
|
||||||
|
Revises: 257a7ce22682
|
||||||
|
Create Date: 2015-09-29 15:32:58.229183
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6190226bed0'
|
||||||
|
down_revision = '257a7ce22682'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column updated_on to the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column(
|
||||||
|
'updated_on',
|
||||||
|
sa.DateTime,
|
||||||
|
nullable=True,
|
||||||
|
default=sa.func.now(),
|
||||||
|
onupdate=sa.func.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute('''UPDATE "pull_requests" SET updated_on=date_created;''')
|
||||||
|
|
||||||
|
op.alter_column(
|
||||||
|
'pull_requests',
|
||||||
|
column_name='updated_on',
|
||||||
|
nullable=False,
|
||||||
|
existing_nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column updated_on from the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_requests', 'updated_on')
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Add the closed_by column to pull_requests
|
||||||
|
|
||||||
|
|
||||||
|
Revision ID: abc71fd60fa
|
||||||
|
Revises: 298891e63039
|
||||||
|
Create Date: 2015-06-08 16:06:18.017110
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'abc71fd60fa'
|
||||||
|
down_revision = '298891e63039'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column merge_status to the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column(
|
||||||
|
'closed_by_id',
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column merge_status from the table pull_requests.
|
||||||
|
'''
|
||||||
|
op.drop_column('pull_requests', 'closed_by_id')
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Add merge status to the pull_requests table
|
||||||
|
|
||||||
|
Revision ID: b5efae6bb23
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2015-06-02 16:30:06.199128
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b5efae6bb23'
|
||||||
|
down_revision = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# Sources for the code: https://bitbucket.org/zzzeek/alembic/issue/67
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
''' Add the column merge_status to the table pull_requests.
|
||||||
|
'''
|
||||||
|
enum = ENUM('NO_CHANGE', 'FFORWARD', 'CONFLICTS', 'MERGE',
|
||||||
|
name='merge_status_enum', create_type=False)
|
||||||
|
enum.create(op.get_bind(), checkfirst=False)
|
||||||
|
op.add_column(
|
||||||
|
'pull_requests',
|
||||||
|
sa.Column('merge_status', enum, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
''' Remove the column merge_status from the table pull_requests.
|
||||||
|
'''
|
||||||
|
ENUM(name="merge_status_enum").drop(op.get_bind(), checkfirst=False)
|
||||||
|
op.drop_column('pull_requests', 'merge_status')
|
14
createdb.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
|
||||||
|
# These two lines are needed to run on EL6
|
||||||
|
__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
from pagure import APP
|
||||||
|
from pagure.lib import model
|
||||||
|
|
||||||
|
model.create_tables(
|
||||||
|
APP.config['DB_URL'],
|
||||||
|
APP.config.get('PATH_ALEMBIC_INI', None),
|
||||||
|
acls=APP.config.get('ACLS', {}),
|
||||||
|
debug=True)
|
153
doc/Makefile
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pagure.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pagure.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/pagure"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pagure"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
BIN
doc/_static/overview.png
vendored
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
doc/_static/overview_simple.png
vendored
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
doc/_static/pagure.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
16
doc/_static/site.css
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import url("cloud.css");
|
||||||
|
@import url("http://fonts.googleapis.com/css?family=Comfortaa");
|
||||||
|
|
||||||
|
.pagure-logo span {
|
||||||
|
background: url("pagure.png") no-repeat scroll 50% 0 transparent;
|
||||||
|
display: block;
|
||||||
|
width: 134px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.pagure-logo {
|
||||||
|
font-family: 'Comfortaa', sans-serif;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
1
doc/_templates/pagure-logo.html
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h1 class='pagure-logo'><span></span>Pagure</h1>
|
5
doc/api.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Pagure API
|
||||||
|
==========
|
||||||
|
|
||||||
|
The API documentation can be found at https://pagure.io/api/0/ or within
|
||||||
|
the sources in ``pagure/doc/api.rst``.
|
317
doc/conf.py
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# pagure documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Wed Mar 1 10:30:13 2015.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing
|
||||||
|
# dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
pagurefile = os.path.join(
|
||||||
|
os.path.dirname(__file__), '..', 'pagure', '__init__.py')
|
||||||
|
|
||||||
|
# Thanks to SQLAlchemy:
|
||||||
|
# https://github.com/zzzeek/sqlalchemy/blob/master/setup.py#L104
|
||||||
|
with open(pagurefile) as stream:
|
||||||
|
VERSION = re.compile(
|
||||||
|
r".*__version__ = '(.*?)'", re.S
|
||||||
|
).match(stream.read()).group(1)
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another
|
||||||
|
# directory, add these directories to sys.path here. If the directory is
|
||||||
|
# relative to the documentation root, use os.path.abspath to make it
|
||||||
|
# absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'pagure'
|
||||||
|
copyright = u'2015, Red Hat Inc, Pierre-Yves Chibon <pingou@pingoured.fr>'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement
|
||||||
|
# for |version| and |release|, also used in various other places throughout
|
||||||
|
# the built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
#version = __version__
|
||||||
|
version = VERSION
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
#release = '1'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['_build']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
|
# documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
import cloud_sptheme as csp
|
||||||
|
|
||||||
|
html_style = 'site.css'
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#html_theme = 'default'
|
||||||
|
html_theme = "cloud"
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#html_theme_options = {}
|
||||||
|
|
||||||
|
html_theme_options = {
|
||||||
|
"sidebarwidth": "200px",
|
||||||
|
"max_width": "900px",
|
||||||
|
"compact_width": "800px",
|
||||||
|
"minimal_width": "700px",
|
||||||
|
|
||||||
|
# Style it like Fedora..
|
||||||
|
"bodyfont": "Cantarell",
|
||||||
|
|
||||||
|
"highlightcolor": "#79db32", # First Green
|
||||||
|
|
||||||
|
"sidebarbgcolor": "#FEFEFE",
|
||||||
|
"sidebartrimcolor": "#FEFEFE",
|
||||||
|
|
||||||
|
"sectionbgcolor": "#FEFEFE",
|
||||||
|
"sectiontrimcolor": "#FEFEFE",
|
||||||
|
"sectiontextcolor": "#444444",
|
||||||
|
|
||||||
|
"relbarbgcolor": "#FEFEFE",
|
||||||
|
"relbartextcolor": "#444444",
|
||||||
|
"relbarlinkcolor": "#444444",
|
||||||
|
|
||||||
|
"bgcolor": "#FEFEFE",
|
||||||
|
"textcolor": "#444444",
|
||||||
|
#"linkcolor": "#79db32", # First Green
|
||||||
|
"linkcolor": "#00009d",
|
||||||
|
|
||||||
|
"headtextcolor": "#444444",
|
||||||
|
"headlinkcolor": "#444444",
|
||||||
|
|
||||||
|
#"codebgcolor"
|
||||||
|
#"codetextcolor"
|
||||||
|
"codetrimcolor": "#79db32", # First Green
|
||||||
|
|
||||||
|
"footerbgcolor": "#FEFEFE",
|
||||||
|
}
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = []
|
||||||
|
html_theme_path = [csp.get_theme_dir()]
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
html_sidebars = {
|
||||||
|
'**': [
|
||||||
|
'pagure-logo.html',
|
||||||
|
'localtoc.html',
|
||||||
|
'relations.html',
|
||||||
|
'sourcelink.html',
|
||||||
|
'searchbox.html',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#html_show_sourcelink = True
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_sphinx = True
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'pagure'
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass
|
||||||
|
# [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
(
|
||||||
|
'index', 'pagure.tex', u'Pagure Documentation',
|
||||||
|
u'Pierre-Yves Chibon \\textless{}pingou@pingoured.fr\\textgreater{}',
|
||||||
|
'manual'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(
|
||||||
|
'index', 'pagure', u'Pagure Documentation',
|
||||||
|
[u'Pierre-Yves Chibon <pingou@pingoured.fr>'],
|
||||||
|
1
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(
|
||||||
|
'index', 'pagure', u'Pagure Documentation',
|
||||||
|
u'Pierre-Yves Chibon <pingou@pingoured.fr>', 'pagure',
|
||||||
|
'Small git-centric forge',
|
||||||
|
'Miscellaneous'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
508
doc/configuration.rst
Normal file
|
@ -0,0 +1,508 @@
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Pagure offers a wide varieties of options that must or can be used to
|
||||||
|
adjust its behavior.
|
||||||
|
|
||||||
|
|
||||||
|
Must options
|
||||||
|
------------
|
||||||
|
|
||||||
|
Here are the options you must set up in order to get pagure running.
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This key is used by flask to create the session. It should be kept secret
|
||||||
|
and set as a long and random string.
|
||||||
|
|
||||||
|
|
||||||
|
SALT_EMAIL
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This key is used for when sending notification to ensure that when sending
|
||||||
|
notifications to different users, each one of them has a different, unique
|
||||||
|
and un-fakable ``Reply-To`` header that is then used by the milter to find
|
||||||
|
out if the response received is a real one or a fake/invalid one.
|
||||||
|
|
||||||
|
|
||||||
|
DB_URL
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
This key indicates to the framework how and where to connect to the database
|
||||||
|
server. Pagure using `SQLAchemy <http://www.sqlalchemy.org/>`_ it can connect
|
||||||
|
to a wide range of database server including MySQL, PostgreSQL and SQLite.
|
||||||
|
|
||||||
|
Examples values:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
DB_URL=mysql://user:pass@host/db_name
|
||||||
|
DB_URL=postgres://user:pass@host/db_name
|
||||||
|
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||||
|
|
||||||
|
Defaults to ``sqlite:////var/tmp/pagure_dev.sqlite``
|
||||||
|
|
||||||
|
|
||||||
|
APP_URL
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
This key indicates the URL at which this pagure instance will be made available.
|
||||||
|
|
||||||
|
Defaults to: ``https://pagure.org/``
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_ERROR
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Pagure sends email when it caches an un-expected error (which saves you from
|
||||||
|
having to monitor the logs regularly but if you like, the error is still
|
||||||
|
present in the logs).
|
||||||
|
This setting allows you to specify to which email address to send these error
|
||||||
|
reports.
|
||||||
|
|
||||||
|
|
||||||
|
GIT_URL_SSH
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key provides the information to the user on how to clone
|
||||||
|
the git repos hosted on pagure via `SSH <https://en.wikipedia.org/wiki/Secure_Shell>`_.
|
||||||
|
|
||||||
|
The URL should end with a slash ``/``.
|
||||||
|
|
||||||
|
Defaults to: ``'ssh://git@pagure.org/'``
|
||||||
|
|
||||||
|
|
||||||
|
GIT_URL_GIT
|
||||||
|
~~~~~~~~~~~
|
||||||
|
This configuration key provides the information to the user on how to clone
|
||||||
|
the git repos hosted on pagure anonymously. This access can be granted via
|
||||||
|
the ``git://`` or ``http(s)://`` protocols.
|
||||||
|
|
||||||
|
The URL should end with a slash ``/``.
|
||||||
|
|
||||||
|
Defaults to: ``'git://pagure.org/'``
|
||||||
|
|
||||||
|
|
||||||
|
GIT_FOLDER
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to where the folders containing the git repos
|
||||||
|
of the projects are located.
|
||||||
|
|
||||||
|
Each project in pagure has 4 git repositories:
|
||||||
|
|
||||||
|
- the main repo for the code
|
||||||
|
- the doc repo showed in the doc server
|
||||||
|
- the ticket and request repos storing the metadata of the
|
||||||
|
tickets/pull-requests
|
||||||
|
|
||||||
|
There are then another 2 folders specifying the locations of the forks and
|
||||||
|
remote git repo used for the remotes pull-requests (ie: pull-request coming
|
||||||
|
from a project not hosted on this instance of pagure).
|
||||||
|
|
||||||
|
|
||||||
|
FORK_FOLDER
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where the git repos of forks of
|
||||||
|
the projects are stored.
|
||||||
|
|
||||||
|
|
||||||
|
DOCS_FOLDER
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where the git repos for the
|
||||||
|
documentation of the projects are stored.
|
||||||
|
|
||||||
|
|
||||||
|
TICKETS_FOLDER
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where the git repos storing the
|
||||||
|
metadata of the tickets opened against the project are stored .
|
||||||
|
|
||||||
|
|
||||||
|
REQUESTS_FOLDER
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where the git repos storing the
|
||||||
|
metadata of the pull-requests opened against the project are stored.
|
||||||
|
|
||||||
|
|
||||||
|
REMOTE_GIT_FOLDER
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where the remote git repos (ie:
|
||||||
|
not hosted on pagure) that someone used to open a pull-request against a
|
||||||
|
project hosted on pagure are stored.
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_COOKIE_SECURE
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When this is set to True, the session cookie will only be returned to the
|
||||||
|
server via ssl (https). If you connect to the server via plain http, the
|
||||||
|
cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||||
|
This may be set to False when testing your application but should always
|
||||||
|
be set to True in production.
|
||||||
|
|
||||||
|
Defaults to: ``False`` for development, must be ``True`` in production with
|
||||||
|
https.
|
||||||
|
|
||||||
|
|
||||||
|
FROM_EMAIL
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This setting allows to specify the email address used by this pagure instance
|
||||||
|
when sending emails (notifications).
|
||||||
|
|
||||||
|
Defaults to: ``pagure@pagure.org``
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN_EMAIL_NOTIFICATIONS
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This setting allows to specify the domain used by this pagure instance
|
||||||
|
when sending emails (notifications). More precisely, this setting is used
|
||||||
|
when building the ``msg-id`` header of the emails sent.
|
||||||
|
|
||||||
|
Defaults to: ``pagure.org``
|
||||||
|
|
||||||
|
|
||||||
|
Configure Gitolite
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Pagure uses `gitolite <http://gitolite.com/>`_ as an authorization layer.
|
||||||
|
Gitolite relies on `SSH <https://en.wikipedia.org/wiki/Secure_Shell>`_ for
|
||||||
|
the authentication. In other words, SSH let you in and gitolite check if you
|
||||||
|
are allowed to do what you are trying to do once you are inside.
|
||||||
|
|
||||||
|
|
||||||
|
GITOLITE_HOME
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key should point to the home of the user under which
|
||||||
|
gitolite is ran.
|
||||||
|
|
||||||
|
|
||||||
|
GITOLITE_VERSION
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to specify which version of gitolite you are
|
||||||
|
using, it can be either ``2`` or ``3``.
|
||||||
|
|
||||||
|
Defaults to: ``3``.
|
||||||
|
|
||||||
|
|
||||||
|
GITOLITE_KEYDIR
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key points to the folder where gitolite stores and accesses
|
||||||
|
the public SSH keys of all the user have access to the server.
|
||||||
|
|
||||||
|
Since pagure is the user interface, it is pagure that writes down the files
|
||||||
|
in this directory effectively setting up the users to be able to use gitolite.
|
||||||
|
|
||||||
|
|
||||||
|
GL_RC
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
This configuration key must point to the file ``gitolite.rc`` used by gitolite
|
||||||
|
to record who has access to what (ie: who has access to which repo/branch).
|
||||||
|
|
||||||
|
|
||||||
|
GL_BINDIR
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the folder in which the gitolite tools can
|
||||||
|
be found. It can be as simple as ``/usr/bin/`` if the tools have been installed
|
||||||
|
using a package manager or something like ``/opt/bin/`` for a more custom
|
||||||
|
install.
|
||||||
|
|
||||||
|
|
||||||
|
EventSource options
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
EVENTSOURCE_SOURCE
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the URL at which the EventSource server is
|
||||||
|
available. If not defined, pagure will behave as if there are no EventSource
|
||||||
|
server running.
|
||||||
|
|
||||||
|
EVENTSOURCE_PORT
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the port at which the EventSource server is
|
||||||
|
running. This allows adjusting the port via the configuration file instead
|
||||||
|
of hard-coding it in the code.
|
||||||
|
|
||||||
|
.. note:: The EventSource server requires a redis server (see ``Redis options``
|
||||||
|
below)
|
||||||
|
|
||||||
|
|
||||||
|
Web-hooks notifications
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
WEBHOOK
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows turning on or off web-hooks notifications for
|
||||||
|
this pagure instance.
|
||||||
|
|
||||||
|
Defaults to: ``False``.
|
||||||
|
|
||||||
|
.. note:: The Web-hooks server requires a redis server (see ``Redis options``
|
||||||
|
below)
|
||||||
|
|
||||||
|
|
||||||
|
Redis options
|
||||||
|
-------------
|
||||||
|
|
||||||
|
REDIS_HOST
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the host at which the `redis <http://redis.io/>`_
|
||||||
|
server is running.
|
||||||
|
|
||||||
|
Defaults to: ``0.0.0.0``.
|
||||||
|
|
||||||
|
REDIS_PORT
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the port at which the reds server can be
|
||||||
|
contacted.
|
||||||
|
|
||||||
|
Defaults to: ``6379``.
|
||||||
|
|
||||||
|
REDIS_DB
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key indicates the name of the redis database to use to
|
||||||
|
communicate with the EventSource server.
|
||||||
|
|
||||||
|
Defaults to: ``0``.
|
||||||
|
|
||||||
|
|
||||||
|
Authentication options
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
ADMIN_GROUP
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
List of groups, local or remotes (if the openid server used supports the
|
||||||
|
group extension), that are site admin. These admins can regenerate the
|
||||||
|
gitolite configuration, the ssh key files, the hook-token for every project
|
||||||
|
as well as manage users and groups.
|
||||||
|
|
||||||
|
|
||||||
|
PAGURE_ADMIN_USERS
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
List of usernames that are site admin. These admins have the same rights as
|
||||||
|
the user in the admin groups (listed above) as well as admin rights to
|
||||||
|
every projects hosted on this pagure instance.
|
||||||
|
|
||||||
|
|
||||||
|
Optional options
|
||||||
|
----------------
|
||||||
|
|
||||||
|
SSH_KEYS
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
It is a good pratice to publish the fingerprint and public SSH key of a
|
||||||
|
server you provide access to.
|
||||||
|
Pagure offers the possibility to expose this information based on the values
|
||||||
|
set in the configuration file, in the ``SSH_KEYS`` configuration key.
|
||||||
|
|
||||||
|
See the `SSH hostkeys/Fingerprints page on pagure.io <https://pagure.io/ssh_info>`_.
|
||||||
|
|
||||||
|
.. warning: The format is important
|
||||||
|
|
||||||
|
SSH_KEYS = {'RSA': {'fingerprint': '<foo>', 'pubkey': '<bar>'}}
|
||||||
|
|
||||||
|
Where `<foo>` and `<bar>` must be replaced by your values.
|
||||||
|
|
||||||
|
|
||||||
|
ITEM_PER_PAGE
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
This configuration key allows you to configure the length of a page by
|
||||||
|
setting the number of items on the page. Items can be commits, users, groups
|
||||||
|
or projects for example.
|
||||||
|
|
||||||
|
Defaults to: ``50``.
|
||||||
|
|
||||||
|
|
||||||
|
SMTP_SERVER
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to configure the SMTP server to use when
|
||||||
|
sending emails.
|
||||||
|
|
||||||
|
Defaults to: ``localhost``.
|
||||||
|
|
||||||
|
SMTP_PORT
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allow to define the SMTP server port.
|
||||||
|
|
||||||
|
SMTP by default uses TCP port 25. The protocol for mail submission is
|
||||||
|
the same, but uses port 587.
|
||||||
|
SMTP connections secured by SSL, known as SMTPS, default to port 465
|
||||||
|
(nonstandard, but sometimes used for legacy reasons).
|
||||||
|
|
||||||
|
Defaults to: ``25``
|
||||||
|
|
||||||
|
SMTP_SSL
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to specify whether the SMTP connections
|
||||||
|
should secured over SSL
|
||||||
|
|
||||||
|
Defaults to: ``False``
|
||||||
|
|
||||||
|
SMTP_USERNAME
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows usage of SMTP with auth
|
||||||
|
|
||||||
|
Note: Specify SMTP_USERNAME and SMTP_PASSWORD for using SMTP auth
|
||||||
|
|
||||||
|
Defaults to: ``None``
|
||||||
|
|
||||||
|
SMTP_PASSWORD
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows usage of SMTP with auth
|
||||||
|
|
||||||
|
Note: Specify SMTP_USERNAME and SMTP_PASSWORD for using SMTP auth
|
||||||
|
|
||||||
|
Defaults to: ``None``
|
||||||
|
|
||||||
|
SHORT_LENGTH
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to configure the length of the commit ids or
|
||||||
|
file hex displayed in the user interface.
|
||||||
|
|
||||||
|
Defaults to: ``6``.
|
||||||
|
|
||||||
|
|
||||||
|
BLACKLISTED_PROJECTS
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to set a list of project name that are forbidden.
|
||||||
|
This list is used for example to avoid conflicts at the URL level between the
|
||||||
|
static files located under ``/static/`` and a project that would be named
|
||||||
|
``static`` and thus be located at ``/static``.
|
||||||
|
|
||||||
|
Defaults to:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
[
|
||||||
|
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||||
|
'logout', 'login', 'users', 'groups'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CHECK_SESSION_IP
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to configure whether to check the user's IP
|
||||||
|
address when retrieving its session. This makes things more secure but
|
||||||
|
under certain setup it might not work (for example if there are proxies
|
||||||
|
in front of the application).
|
||||||
|
|
||||||
|
Defaults to: ``True``.
|
||||||
|
|
||||||
|
|
||||||
|
PAGURE_AUTH
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to specify which authentication method to use.
|
||||||
|
Pagure supports currently two authentication methods, one relying on the
|
||||||
|
Fedora Account System `FAS <https://admin.fedoraproject.org/accounts>`_,
|
||||||
|
the other relying on local user accounts.
|
||||||
|
It can therefore be either ``fas`` or ``local``.
|
||||||
|
|
||||||
|
Defaults to: ``fas``.
|
||||||
|
|
||||||
|
|
||||||
|
IP_ALLOWED_INTERNAL
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to specify which IP addresses are allowed
|
||||||
|
to access the internal API endpoint. These endpoints are accessed by the
|
||||||
|
milters for example and allow to perform action in the name of someone else.
|
||||||
|
So they are sensitive, thus the check for the origin of the request using
|
||||||
|
these endpoints.
|
||||||
|
|
||||||
|
Defaults to: ``['127.0.0.1', 'localhost', '::1']``.
|
||||||
|
|
||||||
|
|
||||||
|
MAX_CONTENT_LENGTH
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to specify the maximum size allowed when
|
||||||
|
uploading content to pagure (for example, screenshots to a ticket).
|
||||||
|
|
||||||
|
Defaults to: ``4 * 1024 * 1024`` which corresponds to 4 megabytes.
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_TICKETS
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to activate or de-activate the ticketing system
|
||||||
|
for all the projects hosted on this pagure instance.
|
||||||
|
|
||||||
|
Defaults to: ``True``
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_NEW_PROJECTS
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to create or forbids creating new projects in
|
||||||
|
the user interface of this pagure instance.
|
||||||
|
|
||||||
|
Defaults to: ``True``
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_DEL_PROJECTS
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows to delete or forbids deleting projects in
|
||||||
|
the user interface of this pagure instance.
|
||||||
|
|
||||||
|
Defaults to: ``True``
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_SEND
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
This configuration key allows turning on or off all email notification for
|
||||||
|
this pagure instance. This can be useful to turn off when developing on
|
||||||
|
pagure, or for test or pre-production instances.
|
||||||
|
|
||||||
|
Defaults to: ``True``.
|
||||||
|
|
||||||
|
|
||||||
|
OLD_VIEW_COMMIT_ENABLED
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In version 1.3, pagure changed its URL scheme to view the commit of a
|
||||||
|
project in order to add support for pseudo-namespaced projects.
|
||||||
|
|
||||||
|
For pagure instances older than 1.3, who care about backward compatibility,
|
||||||
|
we added an endpoint ``view_commit_old`` that brings URL backward
|
||||||
|
compatibility for URLs using the complete git hash (the 40 characters).
|
||||||
|
For URLs using a shorter hash, the URLs will remain broken.
|
||||||
|
|
||||||
|
This configuration key allows turning on or off this backward compatibility
|
||||||
|
which is useful for pagure instances running since before 1.3 but is not
|
||||||
|
for newer instances.
|
||||||
|
|
||||||
|
Defaults to: ``False``.
|
27
doc/contributing.rst
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
If you're submitting patches to pagure, please observe the following:
|
||||||
|
|
||||||
|
- Check that your python code is `PEP8-compliant
|
||||||
|
<http://www.python.org/dev/peps/pep-0008/>`_. There is a `pep8 tool
|
||||||
|
<http://pypi.python.org/pypi/pep8>`_ that can automatically check
|
||||||
|
your source.
|
||||||
|
|
||||||
|
- Check that your code doesn't break the test suite. The test suite can be
|
||||||
|
run using the ``runtests.sh`` shell script at the top of the sources.
|
||||||
|
See :doc:`development` for more information about the test suite.
|
||||||
|
|
||||||
|
- If you are adding new code, please write tests for them in ``tests/``,
|
||||||
|
the ``runtests.sh`` script will help you to see the coverage of your code
|
||||||
|
in unit-tests.
|
||||||
|
|
||||||
|
- If your change warrants a modification to the docs in ``doc/`` or any
|
||||||
|
docstrings in ``pagure/`` please make that modification.
|
||||||
|
|
||||||
|
.. note:: You have a doubt, you don't know how to do something, you have an
|
||||||
|
idea but don't know how to implement it, you just have something bugging
|
||||||
|
you?
|
||||||
|
|
||||||
|
Come to see us on IRC: ``#fedora-apps`` on irc.freenode.net or directly on
|
||||||
|
`the project <http://pagure.io>`_.
|
75
doc/contributors.rst
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
Contributors to pagure
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Pagure would be nothing without its contributors.
|
||||||
|
|
||||||
|
On July 27, 2016 (release 2.3.4), the list looks as follow:
|
||||||
|
|
||||||
|
================= ===========
|
||||||
|
Number of commits Contributor
|
||||||
|
================= ===========
|
||||||
|
4107 Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
174 Ryan Lerch <rlerch@redhat.com>
|
||||||
|
61 farhaanbukhsh <farhaan.bukhsh@gmail.com>
|
||||||
|
59 Johan Cwiklinski <johan@x-tnd.be>
|
||||||
|
48 Clement Verna <cverna@tutanota.com>
|
||||||
|
36 Vivek Anand <vivekanand1101@gmail.com>
|
||||||
|
18 Sayan Chowdhury <sayan.chowdhury2012@gmail.com>
|
||||||
|
15 Gaurav Kumar <aavrug@gmail.com>
|
||||||
|
15 Lubomír Sedlář <lsedlar@redhat.com>
|
||||||
|
15 Patrick Uiterwijk <puiterwijk@redhat.com>
|
||||||
|
15 Ralph Bean <rbean@redhat.com>
|
||||||
|
13 Ghost-script <subho.prp@gmail.com>
|
||||||
|
13 Mathieu Bridon <bochecha@fedoraproject.org>
|
||||||
|
8 Lei Yang <yltt1234512@gmail.com>
|
||||||
|
5 Mike McLean <mikem@redhat.com>
|
||||||
|
5 Oliver Gutierrez <ogutierrez@redhat.com>
|
||||||
|
5 vanzhiganov <vanzhiganov@ya.ru>
|
||||||
|
5 yangl1996 <yltt1234512@gmail.com>
|
||||||
|
4 Maciej Lasyk <maciek@lasyk.info>
|
||||||
|
4 Paul W. Frields <stickster@gmail.com>
|
||||||
|
3 Ankush Behl <cloudbehl@gmail.com>
|
||||||
|
3 Anthony Lackey <alackey96@gmail.com>
|
||||||
|
3 Dhriti Shikhar <dhriti.shikhar.rokz@gmail.com>
|
||||||
|
3 Eric Barbour <ebarbour@redhat.com>
|
||||||
|
3 Jan Pokorný <jpokorny@redhat.com>
|
||||||
|
3 Kushal Khandelwal <kushal124@gmail.com>
|
||||||
|
3 Pedro Lima <pedro.lima@gmail.com>
|
||||||
|
2 Daniel Mach <dmach@redhat.com>
|
||||||
|
2 Nuno Maltez <nuno@cognitiva.com>
|
||||||
|
2 Richard Marko <rmarko@fedoraproject.org>
|
||||||
|
2 Ricky Elrod <ricky@elrod.me>
|
||||||
|
2 Simo Sorce <simo@redhat.com>
|
||||||
|
2 Till Maas <opensource@till.name>
|
||||||
|
2 bruno <bruno@wolff.to>
|
||||||
|
2 dhrish20 <dhrish20@gmail.com>
|
||||||
|
1 Anthony Lackey <alackey@localhost.localdomain>
|
||||||
|
1 David Caro <dcaroest@redhat.com>
|
||||||
|
1 Eric Barbour <emb4gu@virginia.edu>
|
||||||
|
1 Kunaal Jain <kunaalus@gmail.com>
|
||||||
|
1 Mathew Robinson <mathew.robinson3114@gmail.com>
|
||||||
|
1 Pierre-YvesChibon <pingou@fedoraproject.org>
|
||||||
|
1 Rahul Bajaj <rahulrb0509@gmail.com>
|
||||||
|
1 Stanislav Ochotnicky <sochotnicky@redhat.com>
|
||||||
|
1 Vyacheslav Anzhiganov <vanzhiganov@ya.ru>
|
||||||
|
1 Yves Martin <ymartin1040@gmail.com>
|
||||||
|
1 abhishek <abhishekarora12@gmail.com>
|
||||||
|
1 jcvicelli <jcvicelli@gmail.com>
|
||||||
|
1 pingou <pingou@fedoraproject.org>
|
||||||
|
1 ryanlerch <rlerch@redhat.com>
|
||||||
|
1 skrzepto <shims506@gmail.com>
|
||||||
|
1 skrzepto <skrzepto@gmail.com>
|
||||||
|
1 tenstormavi <avi.avinash3008@gmail.com>
|
||||||
|
================= ===========
|
||||||
|
|
||||||
|
This list is generated using
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git shortlog -s -n -e
|
||||||
|
|
||||||
|
|
||||||
|
The old pagure logo has been created by ``Micah Denn <micah.denn@gmail.com>``,
|
||||||
|
the new one, as well as the entire version 2 of the user interface (using
|
||||||
|
bootstrap) is the work of ``Ryan Lerch <rlerch@redhat.com>`` many thanks
|
||||||
|
to them for their work and understanding during the process.
|
261
doc/development.rst
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
Development
|
||||||
|
===========
|
||||||
|
|
||||||
|
Get the sources
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Anonymous:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone https://pagure.io/pagure.git
|
||||||
|
|
||||||
|
Contributors:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone ssh://git@pagure.io:pagure.git
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
The dependencies of pagure are listed in the file ``requirements.txt``
|
||||||
|
at the top level of the sources.
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: working in a `virtualenv <http://www.virtualenv.org/en/latest/>`_
|
||||||
|
is tricky due to the dependency on `pygit2 <http://www.pygit2.org/>`_
|
||||||
|
and thus on `libgit2 <https://libgit2.github.com/>`_
|
||||||
|
but the pygit2 `documentation has a solution for this
|
||||||
|
<http://www.pygit2.org/install.html#libgit2-within-a-virtual-environment>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Run pagure for development
|
||||||
|
--------------------------
|
||||||
|
Adjust the configuration file (secret key, database URL, admin group...)
|
||||||
|
See :doc:`configuration` for more detailed information about the
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
|
||||||
|
Create the database scheme::
|
||||||
|
|
||||||
|
./createdb
|
||||||
|
|
||||||
|
Create the folder that will receive the different git repositories:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir {repos,docs,forks,tickets,requests,remotes}
|
||||||
|
|
||||||
|
|
||||||
|
Run the server:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
./runserver
|
||||||
|
|
||||||
|
If you want to change some configuration key you can create a file, place
|
||||||
|
the configuration change in it and use it with
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
./runserver -c <config_file>
|
||||||
|
|
||||||
|
For example, create the file ``config`` with in it:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
# Makes the admin session longer
|
||||||
|
ADMIN_SESSION_LIFETIME = timedelta(minutes=20000000)
|
||||||
|
|
||||||
|
# Use a postgresql database instead of sqlite
|
||||||
|
DB_URL = 'postgresql://user:pass@localhost/pagure'
|
||||||
|
# Change the OpenID endpoint
|
||||||
|
FAS_OPENID_ENDPOINT = 'https://id.stg.fedoraproject.org'
|
||||||
|
|
||||||
|
APP_URL = '*'
|
||||||
|
EVENTSOURCE_SOURCE = 'http://localhost:8080'
|
||||||
|
EVENTSOURCE_PORT = '8080'
|
||||||
|
DOC_APP_URL = '*'
|
||||||
|
|
||||||
|
# Avoid sending email when developping
|
||||||
|
EMAIL_SEND = False
|
||||||
|
|
||||||
|
and run the server with:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
./runserver -c config
|
||||||
|
|
||||||
|
To get some profiling information you can also run it as:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
./runserver.py --profile
|
||||||
|
|
||||||
|
|
||||||
|
You should be able to access the server at http://localhost:5000
|
||||||
|
|
||||||
|
|
||||||
|
Every time you save a file, the project will be automatically restarted
|
||||||
|
so you can see your change immediatly.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Create a pull-request for testing
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
When working on pagure, it is pretty often that one wanted to work on a
|
||||||
|
feature or a bug related to pull-requests needs to create one.
|
||||||
|
|
||||||
|
Making a pull-request for development purposes isn't hard, if you remember
|
||||||
|
that since you're running a local instance, the git repos created in your
|
||||||
|
pagure instance are also local.
|
||||||
|
|
||||||
|
So here are in a few steps that one could perform to create a pull-request in a
|
||||||
|
local pagure instance.
|
||||||
|
|
||||||
|
* Create a project on your pagure instance, let's say it will be called ``test``
|
||||||
|
|
||||||
|
* Create a folder ``clones`` somewhere in your system (you probably do not
|
||||||
|
want it in the ``repos`` folder created above, next to it is fine though)::
|
||||||
|
|
||||||
|
mkdir clones
|
||||||
|
|
||||||
|
* Clone the repo of the ``test`` project into this ``clones`` folder::
|
||||||
|
|
||||||
|
cd clones
|
||||||
|
git clone ~/path/to/pagure/repos/test.git
|
||||||
|
|
||||||
|
* Add and commit some files::
|
||||||
|
|
||||||
|
echo "*~" > .gitignore
|
||||||
|
git add .gitignore
|
||||||
|
git commit -m "Add a .gitignore file"
|
||||||
|
echo "BSD" > LICENSE
|
||||||
|
git add LICENSE
|
||||||
|
git commit -m "Add a LICENSE file"
|
||||||
|
|
||||||
|
* Push these changes::
|
||||||
|
|
||||||
|
git push -u origin master
|
||||||
|
|
||||||
|
* Create a new branch and add a commit in it::
|
||||||
|
|
||||||
|
git branch new_branch
|
||||||
|
git checkout new_branch
|
||||||
|
touch test
|
||||||
|
git add test
|
||||||
|
git commit -m "Add file: test"
|
||||||
|
|
||||||
|
* Push this new branch::
|
||||||
|
|
||||||
|
git push -u origin new_branch
|
||||||
|
|
||||||
|
|
||||||
|
Then go back to your pagure instance running in your web-browser, check the
|
||||||
|
``test`` project. You should see two branches: ``master`` and ``new_branch``
|
||||||
|
from there you should be able to open a new pull-request, either from the
|
||||||
|
front page or via the ``File Pull Request`` button in the ``Pull Requests``
|
||||||
|
page.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Coding standards
|
||||||
|
----------------
|
||||||
|
|
||||||
|
We are trying to make the code `PEP8-compliant
|
||||||
|
<http://www.python.org/dev/peps/pep-0008/>`_. There is a `pep8 tool
|
||||||
|
<http://pypi.python.org/pypi/pep8>`_ that can automatically check
|
||||||
|
your source.
|
||||||
|
|
||||||
|
|
||||||
|
We are also inspecting the code using `pylint
|
||||||
|
<http://pypi.python.org/pypi/pylint>`_ and aim of course for a 10/10 code
|
||||||
|
(but it is an assymptotic goal).
|
||||||
|
|
||||||
|
.. note:: both pep8 and pylint are available in Fedora via yum:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
yum install python-pep8 pylint
|
||||||
|
|
||||||
|
|
||||||
|
Send patch
|
||||||
|
----------
|
||||||
|
|
||||||
|
The easiest way to work on pagure is to make your own branch in git, make
|
||||||
|
your changes to this branch, commit whenever you want, rebase on master,
|
||||||
|
whenever you need and when you are done, send the patch either by email,
|
||||||
|
via the trac or a pull-request (using git or github).
|
||||||
|
|
||||||
|
|
||||||
|
The workflow would therefore be something like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git branch <my_shiny_feature>
|
||||||
|
git checkout <my_shiny_feature>
|
||||||
|
<work>
|
||||||
|
git commit file1 file2
|
||||||
|
<more work>
|
||||||
|
git commit file3 file4
|
||||||
|
git checkout master
|
||||||
|
git pull
|
||||||
|
git checkout <my_shiny_feature>
|
||||||
|
git rebase master
|
||||||
|
git format-patch -2
|
||||||
|
|
||||||
|
This will create two patch files that you can send by email to submit in a ticket
|
||||||
|
on pagure, by email or after forking the project on pagure by submitting a
|
||||||
|
pull-request (in which case the last step above ``git format-patch -2`` is not
|
||||||
|
needed.
|
||||||
|
|
||||||
|
|
||||||
|
Unit-tests
|
||||||
|
----------
|
||||||
|
|
||||||
|
Pagure has a number of unit-tests.
|
||||||
|
|
||||||
|
|
||||||
|
We aim at having a full (100%) coverage of the whole code (including the
|
||||||
|
Flask application) and of course a smart coverage as in we want to check
|
||||||
|
that the functions work the way we want but also that they fail when we
|
||||||
|
expect it and the way we expect it.
|
||||||
|
|
||||||
|
|
||||||
|
Tests checking that function are failing when/how we want are as important
|
||||||
|
as tests checking they work the way they are intended to.
|
||||||
|
|
||||||
|
``runtests.sh``, located at the top of the sources, helps to run the
|
||||||
|
unit-tests of the project with coverage information using `python-nose
|
||||||
|
<https://nose.readthedocs.org/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: You can specify additional arguments to the nose command used
|
||||||
|
in this script by just passing arguments to the script.
|
||||||
|
|
||||||
|
For example you can specify the ``-x`` / ``--stop`` argument:
|
||||||
|
`Stop running tests after the first error or failure` by just doing
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
./runtests.sh --stop
|
||||||
|
|
||||||
|
|
||||||
|
Each unit-tests files (located under ``tests/``) can be called
|
||||||
|
by alone, allowing easier debugging of the tests. For example:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
python tests/test_pragure_lib.py
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: In order to have coverage information you might have to install
|
||||||
|
``python-coverage``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
yum install python-coverage
|
51
doc/index.rst
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
Pagure
|
||||||
|
=======
|
||||||
|
|
||||||
|
Pagure is a light-weight git-centered forge based on pygit2.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* ``Open-sources``: Web-interface for the git repositories
|
||||||
|
* ``Flexibility``: Ability to create any project you want
|
||||||
|
* ``One place``: Keep your documentation and tickets in pagure
|
||||||
|
* ``Collaboration``: Fork a project and make a pull-request
|
||||||
|
* ``Integration``: Create pull-request from a fork hosted somewhere else than in
|
||||||
|
pagure
|
||||||
|
* ``Open data``: Sources, doc, ticket and pull-requests meta-data are available
|
||||||
|
in the web interface but also in git repos which can thus be cloned and changed
|
||||||
|
locally.
|
||||||
|
* ``Freedom``: Pagure is fully Free and Open-Source Software!
|
||||||
|
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
|
||||||
|
- `Home page <https://pagure.io/>`_
|
||||||
|
- `Git repository <http://pagure.io/pagure>`_
|
||||||
|
- `Github mirror <https://github.com/pypingou/pagure>`_
|
||||||
|
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
|
install
|
||||||
|
install_milter
|
||||||
|
install_evs
|
||||||
|
install_webhooks
|
||||||
|
configuration
|
||||||
|
development
|
||||||
|
usage
|
||||||
|
contributing
|
||||||
|
contributors
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
264
doc/install.rst
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
Installing pagure
|
||||||
|
=================
|
||||||
|
|
||||||
|
There are two ways to install pagure:
|
||||||
|
|
||||||
|
* via the RPM package (recommanded if you are using a RPM-based linux distribution)
|
||||||
|
* via the setup.py
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Installing pagure via RPM
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Here as well there are two ways of obtaining the RPM:
|
||||||
|
|
||||||
|
* From the main repositories
|
||||||
|
|
||||||
|
Pagure is packaged for Fedora since Fedora 21 and is available for RHEL and
|
||||||
|
its derivative via the `EPEL repository <https://fedoraproject.org/wiki/EPEL>`.
|
||||||
|
So installing it is as easy as:
|
||||||
|
::
|
||||||
|
|
||||||
|
dnf install pagure pagure-milters pagure-ev pagure-webhook
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
yum install pagure pagure-milters pagure-ev pagure-webhook
|
||||||
|
|
||||||
|
The ``pagure`` package contains the core of the application and the doc server.
|
||||||
|
(See the ``Overview`` page for a global overview of the structure of the
|
||||||
|
project).
|
||||||
|
|
||||||
|
The ``pagure-milters`` package contains, as the name says, the milter (a
|
||||||
|
mail filter to hook into a MTA).
|
||||||
|
|
||||||
|
The ``pagure-ev`` package contains the eventsource server.
|
||||||
|
|
||||||
|
The ``pagure-webhook`` package contains the web-hook server.
|
||||||
|
|
||||||
|
|
||||||
|
.. note:: The last three packages are optional, pagure would work fine without
|
||||||
|
them but the live-update, the webhook and the comment by email
|
||||||
|
services will not work.
|
||||||
|
|
||||||
|
* From the sources
|
||||||
|
|
||||||
|
If you wish to run a newer version of pagure than what is in the repositories
|
||||||
|
you can easily rebuild it as RPM.
|
||||||
|
|
||||||
|
Simply follow these steps:
|
||||||
|
# Clone the sources::
|
||||||
|
|
||||||
|
git clone https://pagure.io/pagure.git
|
||||||
|
|
||||||
|
# Go to the folder::
|
||||||
|
|
||||||
|
cd pagure
|
||||||
|
|
||||||
|
# Build a tarball of the latest version of pagure::
|
||||||
|
|
||||||
|
python setup.py sdist
|
||||||
|
|
||||||
|
# Build the RPM::
|
||||||
|
|
||||||
|
rpmbuild -ta dist/pagure*.tar.gz
|
||||||
|
|
||||||
|
This will build pagure from the version present in your clone.
|
||||||
|
|
||||||
|
|
||||||
|
Once, the RPM is installed the services ``pagure_milter`` and ``pagure_ev``
|
||||||
|
are ready to be used but the database and the web-application parts still
|
||||||
|
need to be configured.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Installing pagure via setup.py
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Pagure includes in its sources a ``setup.py`` automatint the installation
|
||||||
|
of the web applications of pagure (ie: the core + the doc server).
|
||||||
|
|
||||||
|
|
||||||
|
To install pagure via this mechanism simply follow these steps:
|
||||||
|
# Clone the sources::
|
||||||
|
|
||||||
|
git clone https://pagure.io/pagure.git
|
||||||
|
|
||||||
|
# Go to the folder::
|
||||||
|
|
||||||
|
cd pagure
|
||||||
|
|
||||||
|
# Install the latest version of pagure::
|
||||||
|
|
||||||
|
python setup.py build
|
||||||
|
sudo python setup.py install
|
||||||
|
|
||||||
|
.. note:: To install the eventsource server or the milter, refer to their
|
||||||
|
respective documentations.
|
||||||
|
|
||||||
|
# Install the additional files as follow:
|
||||||
|
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
| Source | Destination |
|
||||||
|
+=============================+===========================================+
|
||||||
|
| ``files/pagure.cfg.sample`` | ``/etc/pagure/pagure.cfg`` |
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
| ``files/alembic.ini`` | ``/etc/pagure/alembic.ini`` |
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
| ``files/pagure.conf`` | ``/etc/httpd/conf.d/pagure.conf`` |
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
| ``files/pagure.wsgi`` | ``/usr/share/pagure/pagure.wsgi`` |
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
| ``createdb.py`` | ``/usr/share/pagure/pagure_createdb.py`` |
|
||||||
|
+------------------------------+------------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Set-up pagure
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Once pagure's files are installed, you still need to set up some things.
|
||||||
|
|
||||||
|
|
||||||
|
* Create the folder release
|
||||||
|
|
||||||
|
This folder is used by project maintainers to upload the tarball of the
|
||||||
|
releases of their project.
|
||||||
|
|
||||||
|
This folder must be accessible by the user under which the application is
|
||||||
|
running (in our case: ``git``).
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /var/www/releases
|
||||||
|
chown git:git /var/www/releases
|
||||||
|
|
||||||
|
|
||||||
|
* Create the folders where the repos, forks and checkouts will be stored
|
||||||
|
|
||||||
|
Pagure stores the sources of a project in a git repo, offers a place to
|
||||||
|
store the project's documentation in another repo, stores a JSON dump of all
|
||||||
|
issues and of all pull-requests in another two repos, and keeps a local
|
||||||
|
checkout of remote projects when asked to do remote pull-requests.
|
||||||
|
All these repositories are stored in different folders that must be
|
||||||
|
created manually.
|
||||||
|
|
||||||
|
For example you can place them under ``/srv/git/repositories/`` which would
|
||||||
|
make ``/srv/git`` the home of your gitolite user.
|
||||||
|
|
||||||
|
You would then create the folders with:
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir /srv/git/repositories/{docs,forks,tickets,requests,remotes}
|
||||||
|
|
||||||
|
|
||||||
|
* Configure apache
|
||||||
|
|
||||||
|
If installed by RPM, you will find an example apache configuration file
|
||||||
|
at: ``/etc/httpd/conf.d/pagure.conf``.
|
||||||
|
|
||||||
|
If not installed by RPM, the example files is present in the sources at:
|
||||||
|
``files/pagure.conf``.
|
||||||
|
|
||||||
|
Adjust it for your needs.
|
||||||
|
|
||||||
|
|
||||||
|
* Configure the WSGI file
|
||||||
|
|
||||||
|
If you installed by RPM, you will find an example WSGI file at:
|
||||||
|
``/usr/share/pagure/pagure.wsgi`` and ``/usr/share/pagure/docs_pagure.wsgi``
|
||||||
|
for the doc server.
|
||||||
|
|
||||||
|
If you did not install by RPM, these files are present in the sources at:
|
||||||
|
``files/pagure.wsgi`` and ``files/doc_pagure.wsgi``.
|
||||||
|
|
||||||
|
Adjust them for your needs
|
||||||
|
|
||||||
|
|
||||||
|
* Give apache permission to read the repositories owned by the ``git`` user.
|
||||||
|
|
||||||
|
For the sake of this document, we assume that the web application runs under
|
||||||
|
the ``git`` user, the same user as your gitolite user, but apache itself
|
||||||
|
runs under the ``httpd`` (or ``apache2``) user. So by default, apache
|
||||||
|
will not be allowed to read git repositories created and managed by gitolite.
|
||||||
|
|
||||||
|
To give apache this permission (required to make git clone via http work),
|
||||||
|
we use file access control lists (aka FACL):
|
||||||
|
::
|
||||||
|
|
||||||
|
setfacl -m user:apache:rx --default
|
||||||
|
setfacl -Rdm user:apache:rx /srv/git
|
||||||
|
setfacl -Rm user:apache:rx /srv/git
|
||||||
|
|
||||||
|
Where ``/srv/git`` is the home of your gitolite user (which will thus need
|
||||||
|
to be adjusted for your configuration).
|
||||||
|
|
||||||
|
|
||||||
|
* Set up the configuration file of pagure
|
||||||
|
|
||||||
|
This is an important step which concerns the file ``/etc/pagure/pagure.cfg``.
|
||||||
|
If you have installed pagure by RPM, this file is already there, otherwise
|
||||||
|
you can find an example one in the sources at: ``files/pagure.cfg.sample``
|
||||||
|
that you will have to copy to the right location.
|
||||||
|
|
||||||
|
Confer the ``Configuration`` section of this documentation for a full
|
||||||
|
explanation of all the options of pagure.
|
||||||
|
|
||||||
|
* Create the database
|
||||||
|
|
||||||
|
You first need to create the database itself. For this, since pagure can
|
||||||
|
work with: `PostgreSQL <http://www.postgresql.org/>`_,
|
||||||
|
`MySQL <http://www.mysql.com/>`_ or `MariaDB <http://mariadb.org/>`_, we
|
||||||
|
would like to invite you to consult the documentation of your database system
|
||||||
|
for this operation.
|
||||||
|
|
||||||
|
Once you have specified in the configuration file the to url used to connect
|
||||||
|
to the database, and create the database itself, you can now create the
|
||||||
|
tables, the database scheme.
|
||||||
|
|
||||||
|
To create the database tables, you need to run the script
|
||||||
|
``/usr/share/pagure/pagure_createdb.py`` and specify it the configuration
|
||||||
|
file to use via an environment variable.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
::
|
||||||
|
|
||||||
|
PAGURE_CONFIG=/etc/pagure/pagure.cfg python /usr/share/pagure/pagure_createdb.py
|
||||||
|
|
||||||
|
This will tell ``/usr/share/pagure/pagure_createdb.py`` to use the database
|
||||||
|
information specified in the file ``/etc/pagure/pagure.cfg``.
|
||||||
|
|
||||||
|
.. warning:: Pagure's default configuration is using sqlite. This is fine
|
||||||
|
for development purpose but not for production use as sqlite does
|
||||||
|
not support all the operations needed when updating the database
|
||||||
|
schema. Do use PostgreSQL, MySQL or MariaDB in production.
|
||||||
|
|
||||||
|
* Stamp the alembic revision
|
||||||
|
|
||||||
|
For changes to existing tables, we rely on `Alembic <http://alembic.readthedocs.org/>`_.
|
||||||
|
It uses `revisions` to perform the upgrades, but to know which upgrades are
|
||||||
|
needed and which are already done, the current revision needs to be saved
|
||||||
|
in the database. This will allow alembic to know apply the new revision when
|
||||||
|
running it.
|
||||||
|
|
||||||
|
You can save the current revision in the database using the following command:
|
||||||
|
::
|
||||||
|
|
||||||
|
cd /etc/pagure
|
||||||
|
alembic stamp $(alembic heads |awk '{ print $1 }')
|
||||||
|
|
||||||
|
The ``cd /etc/pagure`` is needed as the command must be run in the folder
|
||||||
|
where the file ``alembic.ini`` is. This file contains two important pieces
|
||||||
|
of information:
|
||||||
|
|
||||||
|
* ``sqlalchemy.url`` which is the URL used to connect to the database, likely
|
||||||
|
the same URL as the one in ``pagure.cfg``.
|
||||||
|
* ``script_location`` which is the path to the ``versions`` folder containing
|
||||||
|
all the alembic migration files.
|
||||||
|
|
||||||
|
The ``alembic stamp`` command is the one actually saving the current revision
|
||||||
|
into the database. This current revision is found using ``alembic heads``
|
||||||
|
which returns the most recent revision found by alembic, and since the
|
||||||
|
database was just created, it is at the latest revision.
|
48
doc/install_evs.rst
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
Installing pagure's EventSource server
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Eventsource or Server Sent Events are messages sent from a server to a web
|
||||||
|
browser. It allows to refresh a page "live", ie, without the need to reload
|
||||||
|
it entirely.
|
||||||
|
|
||||||
|
|
||||||
|
Configure your system
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The eventsource server is easy to set-up.
|
||||||
|
|
||||||
|
* Install the required dependencies
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
python-redis
|
||||||
|
python-trollius
|
||||||
|
python-trollius-redis
|
||||||
|
|
||||||
|
.. note:: We ship a systemd unit file for pagure_milter but we welcome patches
|
||||||
|
for scripts for other init systems.
|
||||||
|
|
||||||
|
|
||||||
|
* Install the files of the SSE server as follow:
|
||||||
|
|
||||||
|
+----------------------------------------+-----------------------------------------------------+
|
||||||
|
| Source | Destination |
|
||||||
|
+========================================+=====================================================+
|
||||||
|
| ``ev-server/pagure-stream-server.py`` | ``/usr/libexec/pagure-ev/pagure-stream-server.py`` |
|
||||||
|
+----------------------------------------+-----------------------------------------------------+
|
||||||
|
| ``ev-server/pagure_ev.service`` | ``/etc/systemd/system/pagure_ev.service`` |
|
||||||
|
+----------------------------------------+-----------------------------------------------------+
|
||||||
|
|
||||||
|
The first file is the script of the SSE server itself.
|
||||||
|
|
||||||
|
The second file is the systemd service file.
|
||||||
|
|
||||||
|
|
||||||
|
* Finally, activate the service and ensure it's started upon boot:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
systemctl enable redis
|
||||||
|
systemctl start redis
|
||||||
|
systemctl enable pagure_ev
|
||||||
|
systemctl start pagure_ev
|
79
doc/install_milter.rst
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
Installing pagure's milter
|
||||||
|
==========================
|
||||||
|
|
||||||
|
A milter is a script that is ran by a Mail Transfer Agent (`MTA
|
||||||
|
<https://en.wikipedia.org/wiki/Message_transfer_agent>`_)
|
||||||
|
upon receiving an email via either a network or an unix socket.
|
||||||
|
|
||||||
|
If you want more information feel free to check out the corresponding page
|
||||||
|
on wikipedia: `https://en.wikipedia.org/wiki/Milter
|
||||||
|
<https://en.wikipedia.org/wiki/Milter>`_.
|
||||||
|
|
||||||
|
Configure your system
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Install the required dependencies
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
python-pymilter
|
||||||
|
|
||||||
|
.. note:: We ship a systemd unit file for pagure_milter but we welcome patches
|
||||||
|
for scripts for other init systems.
|
||||||
|
|
||||||
|
.. note:: It also requires a MTA, we used postfix.
|
||||||
|
|
||||||
|
|
||||||
|
* Create an alias ``reply``
|
||||||
|
|
||||||
|
This can be done in ``/etc/aliases``, for example:
|
||||||
|
::
|
||||||
|
|
||||||
|
reply: /dev/null
|
||||||
|
|
||||||
|
|
||||||
|
* Activate the ability of your MTA, to split users based on the character ``+``.
|
||||||
|
This way all the emails sent to ``reply+...@example.com`` will be forwarded
|
||||||
|
to your alias for ``reply``.
|
||||||
|
|
||||||
|
|
||||||
|
In postfix this is done via:
|
||||||
|
::
|
||||||
|
|
||||||
|
recipient_delimiter = +
|
||||||
|
|
||||||
|
* Hook the milter in the MTA
|
||||||
|
|
||||||
|
In postfix this is done via:
|
||||||
|
::
|
||||||
|
|
||||||
|
non_smtpd_milters = unix:/var/run/pagure/paguresock
|
||||||
|
smtpd_milters = unix:/var/run/pagure/paguresock
|
||||||
|
|
||||||
|
|
||||||
|
* Install the files of the milter as follow:
|
||||||
|
|
||||||
|
+--------------------------------------+---------------------------------------------------+
|
||||||
|
| Source | Destination |
|
||||||
|
+======================================+===================================================+
|
||||||
|
| ``milters/comment_email_milter.py`` | ``/usr/share//pagure/comment_email_milter.py`` |
|
||||||
|
+--------------------------------------+---------------------------------------------------+
|
||||||
|
| ``milters/milter_tempfile.conf`` | ``/usr/lib/tmpfiles.d/pagure-milter.conf`` |
|
||||||
|
+--------------------------------------+---------------------------------------------------+
|
||||||
|
| ``milters/pagure_milter.service`` | ``/etc/systemd/system/pagure_milter.service`` |
|
||||||
|
+--------------------------------------+---------------------------------------------------+
|
||||||
|
|
||||||
|
The first file is the script of the milter itself.
|
||||||
|
|
||||||
|
The second file is a file specific for systemd and ensuring the temporary
|
||||||
|
folders needed by the milter are re-created if needed at each boot.
|
||||||
|
|
||||||
|
The third file is the systemd service file.
|
||||||
|
|
||||||
|
|
||||||
|
* Activate the service and ensure it's started upon boot:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
systemctl enable pagure_milter
|
||||||
|
systemctl start pagure_milter
|
49
doc/install_webhooks.rst
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
Installing pagure's web-hooks notification system
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
Web-hooks are a notification system upon which a system makes a http POST
|
||||||
|
request with some data upon doing an action. This allows notifying a system
|
||||||
|
that an action has occured.
|
||||||
|
|
||||||
|
If you want more information feel free to check out the corresponding page
|
||||||
|
on wikipedia: `https://en.wikipedia.org/wiki/Webhook
|
||||||
|
<https://en.wikipedia.org/wiki/Webhook>`_.
|
||||||
|
|
||||||
|
Configure your system
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Install the required dependencies
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
python-redis
|
||||||
|
python-trollius
|
||||||
|
python-trollius-redis
|
||||||
|
|
||||||
|
.. note:: We ship a systemd unit file for pagure_webhook but we welcome patches
|
||||||
|
for scripts for other init systems.
|
||||||
|
|
||||||
|
|
||||||
|
* Install the files of the web-hook server as follow:
|
||||||
|
|
||||||
|
+----------------------------------------------+----------------------------------------------------------+
|
||||||
|
| Source | Destination |
|
||||||
|
+==============================================+==========================================================+
|
||||||
|
| ``webhook-server/pagure-webhook-server.py`` | ``/usr/libexec/pagure-webhook/pagure-webhook-server.py`` |
|
||||||
|
+----------------------------------------------+----------------------------------------------------------+
|
||||||
|
| ``webhook-server/pagure_webhook.service`` | ``/etc/systemd/system/pagure_webhook.service`` |
|
||||||
|
+----------------------------------------------+----------------------------------------------------------+
|
||||||
|
|
||||||
|
The first file is the script of the web-hook server itself.
|
||||||
|
|
||||||
|
The second file is the systemd service file.
|
||||||
|
|
||||||
|
|
||||||
|
* Activate the service and ensure it's started upon boot:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
systemctl enable redis
|
||||||
|
systemctl start redis
|
||||||
|
systemctl enable pagure_webhook
|
||||||
|
systemctl start pagure_webhook
|
62
doc/milter.rst
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
Pagure's Milter
|
||||||
|
===============
|
||||||
|
|
||||||
|
`Milter <http://www.postfix.org/MILTER_README.html>`_ are script executed by
|
||||||
|
postfix upon sending or receiving an email.
|
||||||
|
|
||||||
|
We use this system to allow pagure's users to comment on a ticket (or a
|
||||||
|
pull-request) by directly replying to the email sent as a notification.
|
||||||
|
|
||||||
|
Pagure's milter is designed to be run on the same machine as the mail server
|
||||||
|
(postfix by default). Postfix connecting to the milter via a unix socket.
|
||||||
|
|
||||||
|
The milter itself is a service managed by systemd.
|
||||||
|
You can find all the relevant files for the milter under the ``milters`` folder
|
||||||
|
in the sources.
|
||||||
|
|
||||||
|
|
||||||
|
Install the milter
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The first step to enable the milter on a pagure instance is thus to install the
|
||||||
|
``.service`` file for systemd and place the corresponding script that, by
|
||||||
|
default, should go to ``/usr/share/pagure/comment_email_milter.py``.
|
||||||
|
|
||||||
|
If you are using the RPM, install ``pagure-milters`` should provide and install
|
||||||
|
all the files correctly.
|
||||||
|
|
||||||
|
|
||||||
|
Activate the milter
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Make sure the milter is running and will be automaticall started at boot by
|
||||||
|
running the commands:
|
||||||
|
|
||||||
|
To start the milter:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
systemctl start pagure_milter
|
||||||
|
|
||||||
|
To ensure the milter is always started at boot time:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
systemctl enable pagure_milter
|
||||||
|
|
||||||
|
|
||||||
|
Activate the milter in postfix
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
To actually activate the milter in postfix is in fact really easy, all it takes
|
||||||
|
is two lines in the ``main.cf`` file of postfix:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
non_smtpd_milters = unix:/var/run/pagure/paguresock
|
||||||
|
smtpd_milters = unix:/var/run/pagure/paguresock
|
||||||
|
|
||||||
|
These two lines are pointing to the unix socket used by postfix to communicate
|
||||||
|
with the milter. This socket is defined in the milter file itself, in the
|
||||||
|
sources: ``milters/comment_email_milter.py``.
|
||||||
|
|
53
doc/overview.ascii
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
Grants/Denies access
|
||||||
|
+------------+ +-----------+
|
||||||
|
| | | |
|
||||||
|
User's git actions+---------------------->| Gitolite +-------------------------->+ Git repos |
|
||||||
|
| | | |
|
||||||
|
+-----+------+ +-----------+
|
||||||
|
^
|
||||||
|
|
|
||||||
|
|
|
||||||
|
+------------------------------------------+
|
||||||
|
|
|
||||||
|
|
|
||||||
|
+-------------------------+ |
|
||||||
|
Notifications | | |
|
||||||
|
+------------------------------------+ Postfix |<--------------------------------+
|
||||||
|
| | | | |
|
||||||
|
| | +-------------------+ | |
|
||||||
|
| | | | | |
|
||||||
|
v | | Pagure's milter | | |
|
||||||
|
User's mail client | | +--------------+ | |
|
||||||
|
+ +-----+--------+----------+ | | |
|
||||||
|
| ^ Updates | | |
|
||||||
|
| | | | |
|
||||||
|
| Replies | | | |
|
||||||
|
+---------------------------------------------------+ | | |
|
||||||
|
| | |
|
||||||
|
| | |
|
||||||
|
+--------------+ | | |
|
||||||
|
| | | | |
|
||||||
|
+----------------------->| Pagure | v | |
|
||||||
|
| | Doc server | +------------+-+ |
|
||||||
|
| | | |{s} | |
|
||||||
|
| +--------------+ +------->| Database | |
|
||||||
|
| | | | |
|
||||||
|
User's web browser--+ http requests Updates | +--------------+ |
|
||||||
|
^ | & queries| |
|
||||||
|
| | | |
|
||||||
|
| | +--------------+ | |
|
||||||
|
| | | +----------+---------------------------------+
|
||||||
|
| +----------------------->| Pagure |
|
||||||
|
| | web server +---+ +----------------------+ +----------------+
|
||||||
|
| | | | | | | |
|
||||||
|
| +--------------+ | | Pagure | | Third Party |
|
||||||
|
| +---------->| Web hooks' server +-------------->| Services |
|
||||||
|
| | | | | |
|
||||||
|
| redis | +----------------------+ +----------------+
|
||||||
|
| |
|
||||||
|
| | +----------------------+
|
||||||
|
| +---------->| |
|
||||||
|
| | Pagure |
|
||||||
|
+----------------------------------------------------------------------+ EventSource server |
|
||||||
|
Server-Sent Event | |
|
||||||
|
+----------------------+
|
104
doc/overview.rst
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
Pagure is split over multiple components, each having their purpose and all
|
||||||
|
but one (the core application) being optional.
|
||||||
|
|
||||||
|
These components are:
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
Before going into the overall picture, one should realize that most of the
|
||||||
|
components listed above are optional.
|
||||||
|
|
||||||
|
Here is a diagram representing pagure without all the optionals components:
|
||||||
|
|
||||||
|
.. image:: _static/overview_simple.png
|
||||||
|
:target: _static/overview_simple.png
|
||||||
|
|
||||||
|
|
||||||
|
And here is a diagram of all the components together:
|
||||||
|
|
||||||
|
.. image:: _static/overview.png
|
||||||
|
:target: _static/overview.png
|
||||||
|
|
||||||
|
Pagure core application
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The core application is the flask application interacting with gitolite to
|
||||||
|
provide a web UI to the git repositories as well as tickets and pull-requests.
|
||||||
|
This is the main application for the forge.
|
||||||
|
|
||||||
|
|
||||||
|
Gitolite
|
||||||
|
--------
|
||||||
|
|
||||||
|
Currently pagure uses `gitolite <http://gitolite.com/gitolite/index.html>`_
|
||||||
|
to grant or deny `ssh <https://en.wikipedia.org/wiki/Secure_Shell>`_ access
|
||||||
|
to the git repositories, in other words to grant or deny read and/or write
|
||||||
|
access to the git repositories.
|
||||||
|
|
||||||
|
Pagure supports cloning over both ssh and http, but writing can only be done
|
||||||
|
via ssh, through gitolite.
|
||||||
|
|
||||||
|
|
||||||
|
Pagure doc server
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
While integrated into the main application at first, it has been split out
|
||||||
|
for security concern, displaying information directly provided by the user
|
||||||
|
without a clear/safe way of filtering for un-safe script or hacks is a
|
||||||
|
security hole.
|
||||||
|
For this reason we also strongly encourage anyone wanting to deploy their
|
||||||
|
own instance of pagure with the doc server, to run this application on a
|
||||||
|
completely different domain name (not just a sub-domain) in order to reduce
|
||||||
|
the cross-site forgery risks.
|
||||||
|
|
||||||
|
Pagure can be run just fine without the doc server, all you need to do is to
|
||||||
|
**not** define the variable ``DOC_APP_URL`` in the configuration file.
|
||||||
|
|
||||||
|
|
||||||
|
Pagure milter
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The milter is a script, receiving an email as input and performing an action
|
||||||
|
with it.
|
||||||
|
|
||||||
|
In the case of pagure, the milter is used to allow replying on a comment
|
||||||
|
of a ticket or a pull-request by directly replying to the notification sent.
|
||||||
|
No need to go to the page anymore to reply to a comment someone made.
|
||||||
|
|
||||||
|
The milter integrates with a MTA such as postfix or sendmail that you will
|
||||||
|
have running and have access to in order to change its configuration.
|
||||||
|
|
||||||
|
|
||||||
|
Pagure EventSource Server
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Eventsource or Server Sent Events are messages sent from a server to a browser.
|
||||||
|
|
||||||
|
For pagure this technology is used to allow live-refreshing of a page when
|
||||||
|
someone is viewing it. For example, while you are reading a ticket if someone
|
||||||
|
comments on it, the comment will automatically show up on the page without
|
||||||
|
the need for you to reload the entire page.
|
||||||
|
|
||||||
|
The flow is: the main pagure server does an action, sends a message over
|
||||||
|
redis, the eventsource server picks it up and send it to the browsers waiting
|
||||||
|
for it, then javascript code is executed to refresh the page based on the
|
||||||
|
information received.
|
||||||
|
|
||||||
|
|
||||||
|
Pagure web-hook Server
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Sends notifications to third party services using POST http requests.
|
||||||
|
|
||||||
|
This is the second notifications system in pagure with `fedmsg <http://fedmsg.com/>`_.
|
||||||
|
These notifications are running on their own service to prevent blocking the
|
||||||
|
main web application in case the third part service is timing-out or just
|
||||||
|
being slow.
|
||||||
|
|
||||||
|
The flow is: the main pagure server does an action, sends a message over
|
||||||
|
redis, the web-hook server picks it up, build the query and performs the
|
||||||
|
POST request to the specified URLs.
|
26
doc/overview_simple.ascii
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
Grants/Denies access
|
||||||
|
+------------+ +-----------+
|
||||||
|
|cfffca4 | | |
|
||||||
|
User's git actions+--------------------->+ Gitolite +-------------------------->+ Git repos |
|
||||||
|
| | | |
|
||||||
|
+------------+ +-----------+
|
||||||
|
^
|
||||||
|
|
|
||||||
|
+-----------------------------+
|
||||||
|
|
|
||||||
|
User's mail client |
|
||||||
|
^ +---------------+ |
|
||||||
|
| Notifications | | |
|
||||||
|
+----------------------------------+ Mail server | |
|
||||||
|
| | |
|
||||||
|
+---------------+ |
|
||||||
|
^ |
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
+--------------+ Updates +--------------+
|
||||||
|
| | & queries |{s} |
|
||||||
|
User's web browser+---------------------->+ Pagure +------------->| Database |
|
||||||
|
| web server | | |
|
||||||
|
| | +--------------+
|
||||||
|
+--------------+
|
2
doc/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
sphinx
|
||||||
|
cloud_sptheme
|
43
doc/usage.rst
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Using pagure should come fairly easily, especially to people already used
|
||||||
|
to forges such as GitHub or GitLab. There are however some tips and tricks
|
||||||
|
which can be useful to know and that this section of the doc covers.
|
||||||
|
|
||||||
|
|
||||||
|
One of the major difference with GitHub and GitLab is that for each project
|
||||||
|
on pagure, four git repositories are made available to the admins of the
|
||||||
|
project:
|
||||||
|
|
||||||
|
* A git repository containing the source code, displayed in the main section
|
||||||
|
of the pagure project.
|
||||||
|
* A git repository for the documentation
|
||||||
|
* A git repository for the issues and their metadata
|
||||||
|
* A git repository for the metadata for pull-requests
|
||||||
|
|
||||||
|
|
||||||
|
You can find the URLs to access or clone these git repositories on the
|
||||||
|
overview page of the project. On the menu on the right side, there is a menu
|
||||||
|
`Source GIT URLs`, next to it is a little `more` button, by clicking on it
|
||||||
|
you will be given the URLs to the other three git repos.
|
||||||
|
|
||||||
|
Each section correspond to one of the four git repositories created for each
|
||||||
|
project.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
usage/first_steps
|
||||||
|
usage/project_settings
|
||||||
|
usage/roadmap
|
||||||
|
usage/using_doc
|
||||||
|
usage/using_webhooks
|
||||||
|
usage/ticket_templates
|
||||||
|
usage/pr_custom_page
|
||||||
|
usage/theming
|
||||||
|
usage/upgrade_db
|
BIN
doc/usage/_static/pagure_custom_pr.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
doc/usage/_static/pagure_my_settings.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
doc/usage/_static/pagure_roadmap2.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
doc/usage/_static/pagure_ticket_template.png
Normal file
After Width: | Height: | Size: 49 KiB |
80
doc/usage/first_steps.rst
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
First Steps on pagure
|
||||||
|
=====================
|
||||||
|
|
||||||
|
When coming to pagure for the first time there are a few things one should
|
||||||
|
do or check to ensure all works as desired.
|
||||||
|
|
||||||
|
Login to pagure or create your account
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
Pagure has its own user account system.
|
||||||
|
|
||||||
|
For instances of pagure such as the one at `pagure.io <https://pagure.io>`_
|
||||||
|
where the authentication is delegated to a third party (in the case of
|
||||||
|
pagure.io, the Fedora Account System) via OpenID, the local user account
|
||||||
|
is created upon login.
|
||||||
|
|
||||||
|
This means, you cannot be added to a group or a project before you login for
|
||||||
|
the first time as the system will simply not know you.
|
||||||
|
|
||||||
|
If you run your own pagure instance which uses the local authentication
|
||||||
|
system, then you will find on the login page an option to create a new
|
||||||
|
account.
|
||||||
|
|
||||||
|
|
||||||
|
Upload your SSH key
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Pagure uses gitolite to manage who has read/write access to which git
|
||||||
|
repository via `ssh <https://en.wikipedia.org/wiki/Secure_Shell>`_.
|
||||||
|
|
||||||
|
An ssh key is composed of two parts:
|
||||||
|
|
||||||
|
* a private key, which you must keep to yourself and never share with anyone.
|
||||||
|
* a public key, which is public and therefore can be shared with anyone.
|
||||||
|
|
||||||
|
If you have never generated a ssh key, you can do so by running:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
ssh-keygen
|
||||||
|
|
||||||
|
or alternatively on GNOME using the application ``seahorse``.
|
||||||
|
|
||||||
|
This will create two files in ``~/.ssh/`` (``~`` is the symbol for your home
|
||||||
|
folder).
|
||||||
|
|
||||||
|
These two files will be named (for example) ``id_rsa`` and ``id_rsa.pub``.
|
||||||
|
The first one is the private key that must never be shared. The second is
|
||||||
|
the public key that can be uploaded on pagure to give you ssh access.
|
||||||
|
|
||||||
|
To upload your public key onto pagure, login and click on the user icon on
|
||||||
|
the top right corner, there, select ``My settings``.
|
||||||
|
|
||||||
|
.. image:: _static/pagure_my_settings.png
|
||||||
|
:target: _static/pagure_my_settings.png
|
||||||
|
|
||||||
|
|
||||||
|
Configure the default email address
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
If the pagure instance you use is using local user authentication, then when
|
||||||
|
you created your account you could choose whichever email address you prefer
|
||||||
|
to use, but in the case (like pagure.io) where the pagure instance relies
|
||||||
|
on an external authentication service, the email address provided by this
|
||||||
|
service may be different from the one you prefer.
|
||||||
|
|
||||||
|
Your settings' page (cf the image above for how to access to the page) allow
|
||||||
|
you to add multiple email address and set one as default.
|
||||||
|
|
||||||
|
Your default email address is the address that will be used to send you
|
||||||
|
notifications and also as the email address in the git commit if you merge
|
||||||
|
a pull-request with a merge commit.
|
||||||
|
|
||||||
|
For online editing, when doing the commit, you will be presented with the
|
||||||
|
list of valid email addresses associated with your account and you will be
|
||||||
|
able to choose which one you wish to use.
|
||||||
|
|
||||||
|
.. note:: All email address will need to be confirmed to be activated, this
|
||||||
|
is done via a link sent by email to the address. If you do not
|
||||||
|
receive this link, don't forget to check your spam folder!
|
74
doc/usage/pr_custom_page.rst
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
Customize the PR page
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Pagure offers the possibility to customize the page that creates pull-request
|
||||||
|
to add your specific information, such as: please follow the XYZ coding style,
|
||||||
|
run the tests or whatever you wish to inform contributors when they open a
|
||||||
|
new pull-request.
|
||||||
|
|
||||||
|
The customization is done via a file in the git repository containing the
|
||||||
|
meta-data for the pull-requests. This file must be placed under a ``templates``
|
||||||
|
folder, be named ``contributing.md`` and can be formatted as you wish using
|
||||||
|
markdown.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
For a project named ``test`` on ``pagure.io``.
|
||||||
|
|
||||||
|
* First, clone the pull-request git repo [#f1]_ and move into it
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone ssh://git@pagure.io/requests/test.git
|
||||||
|
cd test
|
||||||
|
|
||||||
|
* Create the templates folder
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir templates
|
||||||
|
|
||||||
|
* Create the customized PR info
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
vim templates/contributing.md
|
||||||
|
|
||||||
|
And place in this file the following content:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Contributing to test
|
||||||
|
====================
|
||||||
|
|
||||||
|
When creating a pull-request against test, there are couple of items to do
|
||||||
|
that will speed up the review process:
|
||||||
|
|
||||||
|
* Ensure the unit-tests are all passing (cf the ``runtests.sh`` script at the
|
||||||
|
top level of the sources)
|
||||||
|
* Check if your changes are [pep8](https://www.python.org/dev/peps/pep-0008/)
|
||||||
|
compliant for this you can install ``python-pep8`` and run the ``pep8`` CLI
|
||||||
|
tool
|
||||||
|
|
||||||
|
|
||||||
|
* Commit and push the changes to the git repo
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git add templates
|
||||||
|
git commit -m "Customize the PR page"
|
||||||
|
git push
|
||||||
|
|
||||||
|
|
||||||
|
* And this is how it will look like
|
||||||
|
|
||||||
|
.. image:: _static/pagure_custom_pr.png
|
||||||
|
:target: _static/pagure_custom_pr.png
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. [#f1] All the URLs to the different git repositories can be found on the
|
||||||
|
main page of the project, on the right-side menu, under the section
|
||||||
|
``Source GIT URLs``, click on ``more`` to see them.
|
145
doc/usage/project_settings.rst
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
Project settings
|
||||||
|
================
|
||||||
|
|
||||||
|
Each project have a number of options that can be tweaked in the settings
|
||||||
|
page of the project which is accessible to the person having full commits
|
||||||
|
to the project.
|
||||||
|
|
||||||
|
This page presents the different settings and there effect.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate always merge`
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
This boolean enables or disables always making a merge commit when merging
|
||||||
|
a pull-request.
|
||||||
|
|
||||||
|
When merging a pull-request in pagure there are three states:
|
||||||
|
|
||||||
|
* fast-forward: when the commits in the pull-request can be fast-forwarded
|
||||||
|
pagure signals it and just fast-forward the commit, keeping the history linear.
|
||||||
|
* merge: when the commits in the pull-request cannot be merged without a merge
|
||||||
|
commit, pagure signals it and performs this merge commit.
|
||||||
|
* conflicts: when the commits in the pull-request cannot be merged at all
|
||||||
|
automatically due to one or more conflicts. Then pagure signals it and prevent
|
||||||
|
merging.
|
||||||
|
|
||||||
|
If the `Activate always merge` option is on, then the `fast-forward` option
|
||||||
|
above is disabled in favor of the `merge` option.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate comment editing`
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
This boolean enables or disables editing comments.
|
||||||
|
|
||||||
|
After commenting on a ticket or a pull-request, the admins of the project
|
||||||
|
and the author of the comment may be allowed to edit the comment.
|
||||||
|
This allows them to adjust the wording or the style as they wish.
|
||||||
|
|
||||||
|
.. note:: notification about a comment is only sent once with the original
|
||||||
|
text, changes performed later will not trigger a new notification.
|
||||||
|
|
||||||
|
Some project may not want to allow editing comments after they were posted
|
||||||
|
and this setting allows turning it on or off.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate Enforce signed-off commits in pull-request`
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
This boolean enables or disables checking for a 'Signed-off-by' line (case
|
||||||
|
insensitive) in the commit messages of the pull-requests.
|
||||||
|
|
||||||
|
If this line is missing, pagure will display a message near the `Merge`
|
||||||
|
button, allowing project admin to request the PR to be updated.
|
||||||
|
|
||||||
|
.. note:: This setting does not prevent commits without this 'signed-off-by'
|
||||||
|
line to be pushed directly, it only work at the pull-request level.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate issue tracker`
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
This boolean simply enables or disables the issue tracker for the project.
|
||||||
|
So if you are tracking your ticket on a different system, you can simply
|
||||||
|
disable reporting issue on pagure by un-checking this option.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate Minimum score to merge pull-request`
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
This option can be used for project wishing to enforce having a minimum
|
||||||
|
number of people reviewing a pull-request before it can be merged.
|
||||||
|
|
||||||
|
If this option is enabled, anyone can vote in favor or against a pull-request
|
||||||
|
and the sum of the votes in favor minus the sum of the votes againsts give
|
||||||
|
the pull-request a score that should be equal or great to the value
|
||||||
|
entered in this option for the pull-request to be allowed to be merged.
|
||||||
|
|
||||||
|
.. note:: Only the main comments (ie: not in-line) are taken into account
|
||||||
|
to calculate the score of the pull-request.
|
||||||
|
|
||||||
|
To vote in favor of a pull-request, use either:
|
||||||
|
* ``+1``
|
||||||
|
* ``:thumbsup:``
|
||||||
|
|
||||||
|
To vote against a pull-request, use either:
|
||||||
|
* ``-1``
|
||||||
|
* ``:thumbsdown:``
|
||||||
|
|
||||||
|
.. note:: Pull-Request reaching the minimum score are not automatically merged
|
||||||
|
|
||||||
|
.. note:: Anyone can vote on the pull-request, not only the contributors.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate Only assignee can merge pull-request`
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
This option can be used for project wishing to institute a strong review
|
||||||
|
workflow where pull-request are first assigned then merged.
|
||||||
|
|
||||||
|
If this option is enabled, only the person assigned to the pull-request
|
||||||
|
can merge it.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate project documentation`
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
Pagure offers the option to have a git repository specific for the
|
||||||
|
documentation of the project.
|
||||||
|
|
||||||
|
This repository is then accessible under the ``Docs`` tab in the menu of the
|
||||||
|
project.
|
||||||
|
|
||||||
|
If you prefer to store your documentation elsewhere or maybe even within
|
||||||
|
the sources of the project, you can disable the ``Docs`` tab by un-checking
|
||||||
|
this option.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate pull requests`
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Pagure offers the option to fork a project, make changes to it and then ask
|
||||||
|
the developer to merge these changes into the project. This is similar to
|
||||||
|
the pull-request mechanism on GitHub or GitLab.
|
||||||
|
|
||||||
|
However, some projects may prefer receiving patches by email on their list
|
||||||
|
or via another hosting plateform or simply do not wish to use the
|
||||||
|
pull-request mechanism at all. Un-checking this option will therefore
|
||||||
|
prevent anyone from opening a pull-request against this project.
|
||||||
|
|
||||||
|
.. note:: disabling pull-requests does *not* disable forking the projects.
|
||||||
|
|
||||||
|
|
||||||
|
`Activate Web-hooks`
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Pagure offers the option of sending notification about event happening on a
|
||||||
|
project via [web-hooks|https://en.wikipedia.org/wiki/Webhook]. This option
|
||||||
|
is off by default and can be turned on for a pagure instance in its
|
||||||
|
configuration file.
|
||||||
|
|
||||||
|
The URL of the web-hooks can be entered in this field.
|
||||||
|
|
||||||
|
.. note:: See the ``notifications`` documentation to learn more about
|
||||||
|
web-hooks in pagure and how to use them.
|
35
doc/usage/roadmap.rst
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
Using the roadmap feature
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Pagure allows building the roadmap of the project using the tickets and
|
||||||
|
their tags.
|
||||||
|
|
||||||
|
The principal is as follow:
|
||||||
|
|
||||||
|
* All the ticket with the tag ``roadmap`` will show up on the roadmap page.
|
||||||
|
* For each milestones defined in the settings of the project, the roadmap
|
||||||
|
will group tickets with the corresponding tag.
|
||||||
|
* Tickets with the tag ``roadmap`` that are not associated with any of the
|
||||||
|
milestones defined in the settings are group in an ``unplanned`` section.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
For a project named ``test`` on ``pagure.io``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* First, go to the settings page of the project, create the milestones you
|
||||||
|
like, for example: ``v1.0`` and ``v2.0``.
|
||||||
|
|
||||||
|
* For the tickets you want to be on these milestones, go through each of them
|
||||||
|
and add them the tags: ``roadmap`` in combination with the milestone you want
|
||||||
|
``v1.0`` or ``v2.0``, or none of them if the ticket is on the roadmap but
|
||||||
|
not assigned to any milestones.
|
||||||
|
|
||||||
|
|
||||||
|
* And this is how it will look like
|
||||||
|
|
||||||
|
.. image:: _static/pagure_roadmap2.png
|
||||||
|
:target: _static/pagure_roadmap2.png
|
81
doc/usage/theming.rst
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
Theme your pagure
|
||||||
|
=================
|
||||||
|
|
||||||
|
Pagure via `flask-multistatic <https://pagure.io/flask-multistatic>`_
|
||||||
|
offers the possibility to override the default theme allowing to customize
|
||||||
|
the style of your instance.
|
||||||
|
|
||||||
|
By default pagure looks for its templates and static files in the folders
|
||||||
|
``pagure/templates`` and ``pagure/static``, but you can ask pagure to look
|
||||||
|
for templates and static files in another folder.
|
||||||
|
|
||||||
|
By specifying the configuration keys ``THEME_TEMPLATE_FOLDER`` and
|
||||||
|
``THEME_STATIC_FOLDER`` in pagure's configuration file, you tell pagure to
|
||||||
|
look for templates and static files first in these folders, then in its
|
||||||
|
usual folders.
|
||||||
|
|
||||||
|
|
||||||
|
.. note: The principal is that pagure will look in the folder specified in
|
||||||
|
the configuration file first and then in its usual folder, so the
|
||||||
|
**file names must be identical**.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
Let's take an example, you wish to replace the pagure logo at the top right
|
||||||
|
of all the pages.
|
||||||
|
|
||||||
|
This logo is part of the ``master.html`` template which all pages inherit
|
||||||
|
from. So what you want to do is replace this ``master.html`` by your own.
|
||||||
|
|
||||||
|
* First, create the folder where your templates and static files will be stored:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir /var/www/mypaguretheme/templates
|
||||||
|
mkdir /var/www/mypaguretheme/static
|
||||||
|
|
||||||
|
* Place your own logo in the static folder
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
cp /path/to/your/my-logo.png /var/www/mypaguretheme/static
|
||||||
|
|
||||||
|
* Place in there the original ``master.html``
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
cp /path/to/original/pagure/templates/master.html /var/www/mypaguretheme/templates
|
||||||
|
|
||||||
|
* Edit it and replace the url pointing to the pagure logo (around line 27)
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
- <img height=40px src="{{ url_for('static', filename='pagure-logo.png') }}"
|
||||||
|
+ <img height=40px src="{{ url_for('static', filename='my-logo.png') }}"
|
||||||
|
|
||||||
|
* Adjust pagure's configuration file:
|
||||||
|
|
||||||
|
+ THEME_TEMPLATE_FOLDER='/var/www/mypaguretheme/templates'
|
||||||
|
+ THEME_STATIC_FOLDER='/var/www/mypaguretheme/static'
|
||||||
|
|
||||||
|
* Restart pagure
|
||||||
|
|
||||||
|
|
||||||
|
.. note: you could just have replaced the `pagure-logo.png` file with your
|
||||||
|
own logo which would have avoided overriding the template.
|
||||||
|
|
||||||
|
|
||||||
|
In production
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Serving static files via flask is fine for development but in production
|
||||||
|
you will probably want to have apache server them. This will allow caching
|
||||||
|
either on the server side or on the client side.
|
||||||
|
|
||||||
|
You can ask apache to behave in a similar way as does flask-multistatic with
|
||||||
|
flask here, ie: search in one folder and if you don't find the file look
|
||||||
|
in another one.
|
||||||
|
|
||||||
|
`An example apache configuration <https://pagure.io/flask-multistatic/blob/master/f/example.conf>`_
|
||||||
|
is provided as part of the sources of `flask-multistatic <https://pagure.io/flask-multistatic/>`_.
|
77
doc/usage/ticket_templates.rst
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
Templates for ticket input
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Pagure offers the possibility to add templates for ticket's input. These
|
||||||
|
templates do not enforce anything, users will have the possibility to simply
|
||||||
|
ignore it, or even to not follow it, but it also helps structuring the
|
||||||
|
ticket opened against a project and highlighting the information that are
|
||||||
|
often requested/needed.
|
||||||
|
|
||||||
|
The templates are provided in the git repository containing the meta-data
|
||||||
|
for the tickets.
|
||||||
|
They must be placed under a ``templates`` folder in this git repository,
|
||||||
|
end with ``.md``and as the extension suggests can be formated as markdown.
|
||||||
|
|
||||||
|
If you create a template ``templates/default.md``, it will be shown by
|
||||||
|
default when someone ask to create a new ticket.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
For a project named ``test`` on ``pagure.io``.
|
||||||
|
|
||||||
|
* First, clone the ticket git repo [#f1]_ and move into it
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone ssh://git@pagure.io/tickets/pagure.git
|
||||||
|
cd test
|
||||||
|
|
||||||
|
* Create the templates folder
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir templates
|
||||||
|
|
||||||
|
* Create a default template
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
vim templates/default.md
|
||||||
|
|
||||||
|
And place in this file the following content:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
##### Issue
|
||||||
|
|
||||||
|
##### Steps to reproduce
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
##### Actual results
|
||||||
|
|
||||||
|
##### Expected results
|
||||||
|
|
||||||
|
* Commit and push the changes to the git repo
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git add templates
|
||||||
|
git commit -m "Add a default template for tickets"
|
||||||
|
git push
|
||||||
|
|
||||||
|
|
||||||
|
* And this is how it will look like
|
||||||
|
|
||||||
|
.. image:: _static/pagure_ticket_template.png
|
||||||
|
:target: _static/pagure_ticket_template.png
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. [#f1] All the URLs to the different git repositories can be found on the
|
||||||
|
main page of the project, on the right-side menu, under the section
|
||||||
|
``Source GIT URLs``, click on ``more`` to see them.
|
48
doc/usage/upgrade_db.rst
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
Upgrade a database
|
||||||
|
==================
|
||||||
|
|
||||||
|
|
||||||
|
Database schema migration are handled in two ways:
|
||||||
|
|
||||||
|
* New tables
|
||||||
|
|
||||||
|
For this we simply rely on the ``createdb`` script used when creating the
|
||||||
|
database the first time.
|
||||||
|
|
||||||
|
* Changes to existing tables
|
||||||
|
|
||||||
|
For changes to existing tables, we rely on `Alembic <http://alembic.readthedocs.org/>`_.
|
||||||
|
This allows us to do upgrade and downgrade of schema migration, kind of like
|
||||||
|
one would do commits in a system like git.
|
||||||
|
|
||||||
|
To upgrade the database to the latest version simply run:
|
||||||
|
::
|
||||||
|
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
This may fail for different reasons:
|
||||||
|
|
||||||
|
* The change was already made in the database
|
||||||
|
|
||||||
|
This can be because the version of the database schema saved is incorrect.
|
||||||
|
It can be debugged using the following commands:
|
||||||
|
|
||||||
|
* Find the current revision: ``alembic current``
|
||||||
|
* See the entire history: ``alembic history``
|
||||||
|
|
||||||
|
Once the revision at which your database should be is found (in the history)
|
||||||
|
you can declare that your database is at this given revision using:
|
||||||
|
``alembic stamp <revision id>``.
|
||||||
|
|
||||||
|
Eventually, if you do not know where your database is or should be, you can
|
||||||
|
do an iterative process stamping the database for every revision, one by one
|
||||||
|
trying everytime to ``alembic upgrade`` until it works.
|
||||||
|
|
||||||
|
* The database used does not support some of the changes
|
||||||
|
|
||||||
|
SQLite is handy for development but does not support all the features of a
|
||||||
|
real database server. Upgrading a SQLite database might therefore not work,
|
||||||
|
depending on the changes done.
|
||||||
|
|
||||||
|
In some cases, if you are using a SQLite database, you will have to destroy
|
||||||
|
it and create a new one.
|
108
doc/usage/using_doc.rst
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
Using the doc repository of your project
|
||||||
|
========================================
|
||||||
|
|
||||||
|
In this section of the documentation, we are interested in the doc repository.
|
||||||
|
|
||||||
|
The doc repository is a simple git repo, whose content will appear under the
|
||||||
|
`Docs` tab in pagure and on https://docs.pagure.org/<project>/.
|
||||||
|
|
||||||
|
There are a few ways you can put your documentation in this repo:
|
||||||
|
|
||||||
|
* Simple text files
|
||||||
|
|
||||||
|
Pagure will display them as plain text. If one of these is named ``index``
|
||||||
|
it will be presented as the front page.
|
||||||
|
|
||||||
|
* rst or markdown files
|
||||||
|
|
||||||
|
Pagure will convert them to html on the fly and display them as such.
|
||||||
|
The rst files must end with `.rst` and the markdown ones must end with
|
||||||
|
``.mk``, ``.md`` or simply ``.markdown``.
|
||||||
|
|
||||||
|
* html files
|
||||||
|
|
||||||
|
Pagure will simply show them as such.
|
||||||
|
|
||||||
|
|
||||||
|
.. note: By default the `Docs` tab in the project's menu is disabled, you
|
||||||
|
will have to visit the project's settings page and turn it on
|
||||||
|
in the ``Project options`` section.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
Pagure's documentation is kept in pagure's sources, in the `doc` folder there.
|
||||||
|
You can see it at: `https://pagure.io/pagure/blob/master/f/doc
|
||||||
|
<https://pagure.io/pagure/blob/master/f/doc>`_. This doc can be built with
|
||||||
|
`sphinx <http://sphinx-doc.org/>`_ to make it html and prettier.
|
||||||
|
|
||||||
|
The built documentation is available at: `https://docs.pagure.org/pagure/
|
||||||
|
<https://docs.pagure.org/pagure/>`_.
|
||||||
|
|
||||||
|
This is how it is built/updated:
|
||||||
|
|
||||||
|
* Clone pagure's sources::
|
||||||
|
|
||||||
|
git clone https://pagure.io/docs/pagure.git
|
||||||
|
|
||||||
|
* Move into its doc folder::
|
||||||
|
|
||||||
|
cd pagure/doc
|
||||||
|
|
||||||
|
* Build the doc::
|
||||||
|
|
||||||
|
make html
|
||||||
|
|
||||||
|
* Clone pagure's doc repository::
|
||||||
|
|
||||||
|
git clone ssh://git@pagure.io/docs/pagure.git
|
||||||
|
|
||||||
|
* Copy the result of sphinx's build to the doc repo::
|
||||||
|
|
||||||
|
cp -r _build/html/* pagure/
|
||||||
|
|
||||||
|
* Go into the doc repo and update it::
|
||||||
|
|
||||||
|
cd pagure
|
||||||
|
git add .
|
||||||
|
git commit -am "Update documentation"
|
||||||
|
git push
|
||||||
|
|
||||||
|
* Clean the sources::
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
rm -rf pagure # remove the doc repo
|
||||||
|
rm -rf _build # remove the output from the sphinx's build
|
||||||
|
|
||||||
|
|
||||||
|
To make things simpler, the following script (name `update_doc.sh`) can be
|
||||||
|
used:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
make html
|
||||||
|
|
||||||
|
git clone "ssh://git@pagure.io/docs/$1.git"
|
||||||
|
cp -r _build/html/* $1/
|
||||||
|
(
|
||||||
|
cd $1
|
||||||
|
git add .
|
||||||
|
git commit -av
|
||||||
|
git push
|
||||||
|
)
|
||||||
|
|
||||||
|
rm -rfI _build
|
||||||
|
rm -rfI $1
|
||||||
|
|
||||||
|
It can be used by running `update_doc.sh <project>` from within the folder
|
||||||
|
containing the doc.
|
||||||
|
|
||||||
|
So for pagure it would be something like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
cd pagure/doc
|
||||||
|
update_doc.sh pagure
|
57
doc/usage/using_webhooks.rst
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
Using web-hooks
|
||||||
|
===============
|
||||||
|
|
||||||
|
Web-hooks are a notification system that could be compared to a callback.
|
||||||
|
Basically, pagure will make a HTTP POST request to one or more third party
|
||||||
|
server/application with information about what is or just happened.
|
||||||
|
|
||||||
|
To set-up a web-hook, simply go to the settings page of your project and
|
||||||
|
enter the URL to the server/endpoint that will receive the notifications.
|
||||||
|
|
||||||
|
There is, in the settings page, a web-hook key which is used by the
|
||||||
|
server (here pagure) to sign the message sent and which you can use to
|
||||||
|
ensure the notifications received are coming from the right source.
|
||||||
|
|
||||||
|
Each POST request made contains two specific headers:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
X-Pagure-Topic
|
||||||
|
X-Pagure-Signature
|
||||||
|
|
||||||
|
|
||||||
|
``X-Pagure-Topic`` is a global header giving a clue about the type of action
|
||||||
|
that just occured. For example ``issue.edit``.
|
||||||
|
|
||||||
|
|
||||||
|
``X-Pagure-Signature`` contains the signature of the message allowing to
|
||||||
|
check that the message comes from pagure.
|
||||||
|
|
||||||
|
.. warning:: These headers are present for convenience only, they are not
|
||||||
|
signed and therefore should not be trusted. Rely on the payload
|
||||||
|
after checking the signature to make any decision.
|
||||||
|
|
||||||
|
Pagure relies on ``hmac`` to sign the content of its messages. If you want
|
||||||
|
to validate the message, in python, you can do something like the following:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
payload = # content you received in the POST request
|
||||||
|
headers = # headers of the POST request
|
||||||
|
project_web_hook_key = # private web-hook key of the project
|
||||||
|
|
||||||
|
hashhex = hmac.new(
|
||||||
|
str(project_web_hook_key), payload, hashlib.sha1).hexdigest()
|
||||||
|
|
||||||
|
if hashhex != headers.get('X-Pagure-Signature'):
|
||||||
|
raise Exception('Message received with an invalid signature')
|
||||||
|
|
||||||
|
|
||||||
|
The notifications sent via web-hooks have the same payload as what is sent
|
||||||
|
via `fedmsg <http://www.fedmsg.com/en/latest/>`_. Therefore, the list of
|
||||||
|
pagure topics as well as example messages can be found in the
|
||||||
|
`fedmsg documentation about pagure
|
||||||
|
<https://fedora-fedmsg.readthedocs.org/en/latest/topics.html#id532>`_
|
248
ev-server/pagure-stream-server.py
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
|
||||||
|
Streaming server for pagure's eventsource feature
|
||||||
|
This server takes messages sent to redis and publish them at the specified
|
||||||
|
endpoint
|
||||||
|
|
||||||
|
To test, run this script and in another terminal
|
||||||
|
nc localhost 8080
|
||||||
|
HELLO
|
||||||
|
|
||||||
|
GET /test/issue/26?foo=bar HTTP/1.1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import trollius
|
||||||
|
import trollius_redis
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||||
|
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.lib
|
||||||
|
from pagure.exceptions import PagureEvException
|
||||||
|
|
||||||
|
SERVER = None
|
||||||
|
|
||||||
|
def get_obj_from_path(path):
|
||||||
|
""" Return the Ticket or Request object based on the path provided.
|
||||||
|
"""
|
||||||
|
username = None
|
||||||
|
try:
|
||||||
|
if path.startswith('/fork'):
|
||||||
|
username, repo, obj, objid = path.split('/')[2:6]
|
||||||
|
else:
|
||||||
|
repo, obj, objid = path.split('/')[1:4]
|
||||||
|
except:
|
||||||
|
raise PagureEvException("Invalid URL: %s" % path)
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(pagure.SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise PagureEvException("Project '%s' not found" % repo)
|
||||||
|
|
||||||
|
output = None
|
||||||
|
if obj == 'issue':
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise PagureEvException("No issue tracker found for this project")
|
||||||
|
|
||||||
|
output = pagure.lib.search_issues(
|
||||||
|
pagure.SESSION, repo, issueid=objid)
|
||||||
|
|
||||||
|
if output is None or output.project != repo:
|
||||||
|
raise PagureEvException("Issue '%s' not found" % objid)
|
||||||
|
|
||||||
|
if output.private:
|
||||||
|
# TODO: find a way to do auth
|
||||||
|
raise PagureEvException(
|
||||||
|
"This issue is private and you are not allowed to view it")
|
||||||
|
elif obj == 'pull-request':
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise PagureEvException(
|
||||||
|
"No pull-request tracker found for this project")
|
||||||
|
|
||||||
|
output = pagure.lib.search_pull_requests(
|
||||||
|
pagure.SESSION, project_id=repo.id, requestid=objid)
|
||||||
|
|
||||||
|
if output is None or output.project != repo:
|
||||||
|
raise PagureEvException("Pull-Request '%s' not found" % objid)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise PagureEvException("Invalid object provided: '%s'" % obj)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@trollius.coroutine
|
||||||
|
def handle_client(client_reader, client_writer):
|
||||||
|
data = None
|
||||||
|
while True:
|
||||||
|
# give client a chance to respond, timeout after 10 seconds
|
||||||
|
line = yield trollius.From(trollius.wait_for(
|
||||||
|
client_reader.readline(),
|
||||||
|
timeout=10.0))
|
||||||
|
if not line.decode().strip():
|
||||||
|
break
|
||||||
|
line = line.decode().rstrip()
|
||||||
|
if data is None:
|
||||||
|
data = line
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
log.warning("Expected ticket uid, received None")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = data.decode().rstrip().split()
|
||||||
|
log.info("Received %s", data)
|
||||||
|
if not data:
|
||||||
|
log.warning("No URL provided: %s" % data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not '/' in data[1]:
|
||||||
|
log.warning("Invalid URL provided: %s" % data[1])
|
||||||
|
return
|
||||||
|
|
||||||
|
url = urlparse.urlsplit(data[1])
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = get_obj_from_path(url.path)
|
||||||
|
except PagureEvException as err:
|
||||||
|
log.warning(err.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
origin = pagure.APP.config.get('APP_URL')
|
||||||
|
if origin.endswith('/'):
|
||||||
|
origin = origin[:-1]
|
||||||
|
|
||||||
|
client_writer.write((
|
||||||
|
"HTTP/1.0 200 OK\n"
|
||||||
|
"Content-Type: text/event-stream\n"
|
||||||
|
"Cache: nocache\n"
|
||||||
|
"Connection: keep-alive\n"
|
||||||
|
"Access-Control-Allow-Origin: %s\n\n" % origin
|
||||||
|
).encode())
|
||||||
|
|
||||||
|
connection = yield trollius.From(trollius_redis.Connection.create(
|
||||||
|
host=pagure.APP.config['REDIS_HOST'],
|
||||||
|
port=pagure.APP.config['REDIS_PORT'],
|
||||||
|
db=pagure.APP.config['REDIS_DB']))
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Create subscriber.
|
||||||
|
subscriber = yield trollius.From(connection.start_subscribe())
|
||||||
|
|
||||||
|
# Subscribe to channel.
|
||||||
|
yield trollius.From(subscriber.subscribe(['pagure.%s' % obj.uid]))
|
||||||
|
|
||||||
|
# Inside a while loop, wait for incoming events.
|
||||||
|
while True:
|
||||||
|
reply = yield trollius.From(subscriber.next_published())
|
||||||
|
#print(u'Received: ', repr(reply.value), u'on channel', reply.channel)
|
||||||
|
log.info(reply)
|
||||||
|
log.info("Sending %s", reply.value)
|
||||||
|
client_writer.write(('data: %s\n\n' % reply.value).encode())
|
||||||
|
yield trollius.From(client_writer.drain())
|
||||||
|
|
||||||
|
except trollius.ConnectionResetError as err:
|
||||||
|
log.exception("ERROR: ConnectionResetError in handle_client")
|
||||||
|
except Exception as err:
|
||||||
|
log.exception("ERROR: Exception in handle_client")
|
||||||
|
finally:
|
||||||
|
# Wathever happens, close the connection.
|
||||||
|
connection.close()
|
||||||
|
client_writer.close()
|
||||||
|
|
||||||
|
|
||||||
|
@trollius.coroutine
|
||||||
|
def stats(client_reader, client_writer):
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info('Clients: %s', SERVER.active_count)
|
||||||
|
client_writer.write((
|
||||||
|
"HTTP/1.0 200 OK\n"
|
||||||
|
"Cache: nocache\n\n"
|
||||||
|
).encode())
|
||||||
|
client_writer.write(('data: %s\n\n' % SERVER.active_count).encode())
|
||||||
|
yield trollius.From(client_writer.drain())
|
||||||
|
|
||||||
|
except trollius.ConnectionResetError as err:
|
||||||
|
log.info(err)
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
client_writer.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global SERVER
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop = trollius.get_event_loop()
|
||||||
|
coro = trollius.start_server(
|
||||||
|
handle_client,
|
||||||
|
host=None,
|
||||||
|
port=pagure.APP.config['EVENTSOURCE_PORT'],
|
||||||
|
loop=loop)
|
||||||
|
SERVER = loop.run_until_complete(coro)
|
||||||
|
log.info('Serving server at {}'.format(SERVER.sockets[0].getsockname()))
|
||||||
|
if pagure.APP.config.get('EV_STATS_PORT'):
|
||||||
|
stats_coro = trollius.start_server(
|
||||||
|
stats,
|
||||||
|
host=None,
|
||||||
|
port=pagure.APP.config.get('EV_STATS_PORT'),
|
||||||
|
loop=loop)
|
||||||
|
stats_server = loop.run_until_complete(stats_coro)
|
||||||
|
log.info('Serving stats at {}'.format(
|
||||||
|
stats_server.sockets[0].getsockname()))
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
except trollius.ConnectionResetError as err:
|
||||||
|
log.exception("ERROR: ConnectionResetError in main")
|
||||||
|
except Exception as err:
|
||||||
|
log.exception("ERROR: Exception in main")
|
||||||
|
finally:
|
||||||
|
# Close the server
|
||||||
|
SERVER.close()
|
||||||
|
if pagure.APP.config.get('EV_STATS_PORT'):
|
||||||
|
stats_server.close()
|
||||||
|
log.info("End Connection")
|
||||||
|
loop.run_until_complete(SERVER.wait_closed())
|
||||||
|
loop.close()
|
||||||
|
log.info("End")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
log = logging.getLogger("")
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
"%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
|
||||||
|
|
||||||
|
# setup console logging
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
aslog = logging.getLogger("asyncio")
|
||||||
|
aslog.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
log.addHandler(ch)
|
||||||
|
main()
|
14
ev-server/pagure_ev.service
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Pagure EventSource server (Allowing live refresh of the pages supporting it)
|
||||||
|
After=redis.target
|
||||||
|
Documentation=https://pagure.io/pagure
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/libexec/pagure-ev/pagure-stream-server.py
|
||||||
|
Type=simple
|
||||||
|
User=git
|
||||||
|
Group=git
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
60
files/alembic.ini
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = /usr/share/pagure/alembic
|
||||||
|
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
#truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
#sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
sqlalchemy.url = sqlite:////var/tmp/pagure_dev.sqlite
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
55
files/api_key_expire_mail.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||||
|
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
from pagure import SESSION
|
||||||
|
from pagure.lib import model
|
||||||
|
|
||||||
|
|
||||||
|
def main(debug=False):
|
||||||
|
''' The function that actually sends the email
|
||||||
|
in case the expiration date is near'''
|
||||||
|
|
||||||
|
current_time = datetime.utcnow()
|
||||||
|
day_diff_for_mail = [5, 3, 1]
|
||||||
|
email_dates = [email_day.date() for email_day in \
|
||||||
|
[current_time + timedelta(days=i) for i in day_diff_for_mail]]
|
||||||
|
|
||||||
|
tokens = SESSION.query(model.Token).all()
|
||||||
|
|
||||||
|
for token in tokens:
|
||||||
|
if token.expiration.date() in email_dates:
|
||||||
|
user = token.user
|
||||||
|
user_email = user.default_email
|
||||||
|
project = token.project
|
||||||
|
days_left = token.expiration.day - datetime.utcnow().day
|
||||||
|
subject = 'Pagure API key expiration date is near!'
|
||||||
|
text = '''Hi %s, \nYour Pagure API key for the project %s will expire
|
||||||
|
in %s day(s). Please get a new key for non-interrupted service. \n
|
||||||
|
Thanks, \nYour Pagure Admin. ''' % (user.fullname, project.name, days_left)
|
||||||
|
msg = pagure.lib.notify.send_email(text, subject, user_email)
|
||||||
|
if debug:
|
||||||
|
print 'Sent mail to %s' % user.fullname
|
||||||
|
if debug:
|
||||||
|
print 'Done'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Script to send email before the api token expires')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug', dest='debug', action='store_true', default=False,
|
||||||
|
help='Print the debugging output')
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(debug=args.debug)
|
23
files/doc_pagure.wsgi
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The three lines below are required to run on EL6 as EL6 has
|
||||||
|
# two possible version of python-sqlalchemy and python-jinja2
|
||||||
|
# These lines make sure the application uses the correct version.
|
||||||
|
import __main__
|
||||||
|
__main__.__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
# Set the environment variable pointing to the configuration file
|
||||||
|
import os
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
|
||||||
|
# The following is only needed if you did not install pagure
|
||||||
|
# as a python module (for example if you run it from a git clone).
|
||||||
|
#import sys
|
||||||
|
#sys.path.insert(0, '/path/to/pagure/')
|
||||||
|
|
||||||
|
|
||||||
|
# The most import line to make the wsgi working
|
||||||
|
from pagure.docs_server import APP as application
|
||||||
|
#application.debug = True
|
32
files/emoji_clean_json.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
data = None
|
||||||
|
with open('emoji_strategy.json') as stream:
|
||||||
|
data = json.load(stream)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
print 'Could not load the data from the JSON file'
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Retrieve the items we keep in the JSON
|
||||||
|
tokeep = {}
|
||||||
|
for key in data:
|
||||||
|
if '-' in data[key]['unicode'] and data[key]['unicode'].startswith('1F'):
|
||||||
|
continue
|
||||||
|
tokeep[key] = data[key]
|
||||||
|
|
||||||
|
# Check if we have the keys of all images we kept
|
||||||
|
|
||||||
|
unicodes = [tokeep[key]['unicode'] for key in tokeep]
|
||||||
|
images = [item.replace('.png', '') for item in os.listdir('png')]
|
||||||
|
|
||||||
|
print set(unicodes).symmetric_difference(set(images))
|
||||||
|
|
||||||
|
|
||||||
|
with open('emoji_strategy2.json', 'w') as stream:
|
||||||
|
json.dump(tokeep, stream)
|
||||||
|
|
233
files/gitolite.rc
Executable file
|
@ -0,0 +1,233 @@
|
||||||
|
# paths and configuration variables for gitolite
|
||||||
|
|
||||||
|
# please read comments before editing
|
||||||
|
|
||||||
|
# this file is meant to be pulled into a perl program using "do" or "require".
|
||||||
|
|
||||||
|
# You do NOT need to know perl to edit the paths; it should be fairly
|
||||||
|
# self-explanatory and easy to maintain perl syntax :-)
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# Do not uncomment these values unless you know what you're doing
|
||||||
|
# $GL_PACKAGE_CONF = "";
|
||||||
|
# $GL_PACKAGE_HOOKS = "";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# this is where the repos go. If you provide a relative path (not starting
|
||||||
|
# with "/"), it's relative to your $HOME. You may want to put in something
|
||||||
|
# like "/bigdisk" or whatever if your $HOME is too small for the repos, for
|
||||||
|
# example
|
||||||
|
|
||||||
|
$REPO_BASE="/srv/git/repositories/";
|
||||||
|
|
||||||
|
# the default umask for repositories is 0077; change this if you run stuff
|
||||||
|
# like gitweb and find it can't read the repos. Please note the syntax; the
|
||||||
|
# leading 0 is required
|
||||||
|
|
||||||
|
$REPO_UMASK = 0002;
|
||||||
|
# $REPO_UMASK = 0027; # gets you 'rwxr-x---'
|
||||||
|
# $REPO_UMASK = 0022; # gets you 'rwxr-xr-x'
|
||||||
|
|
||||||
|
# part of the setup of gitweb is a variable called $projects_list (please see
|
||||||
|
# gitweb documentation for more on this). Set this to the same value:
|
||||||
|
|
||||||
|
$PROJECTS_LIST = $ENV{HOME} . "/projects.list";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# I see no reason anyone may want to change the gitolite admin directory, but
|
||||||
|
# feel free to do so. However, please note that it *must* be an *absolute*
|
||||||
|
# path (i.e., starting with a "/" character)
|
||||||
|
|
||||||
|
# gitolite admin directory, files, etc
|
||||||
|
|
||||||
|
$GL_ADMINDIR="/etc/gitolite";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# templates for location of the log files and format of their names
|
||||||
|
|
||||||
|
# I prefer this template (note the %y and %m placeholders)
|
||||||
|
# it produces files like `~/.gitolite/logs/gitolite-2009-09.log`
|
||||||
|
|
||||||
|
$GL_LOGT="/var/log/gitolite/gitolite-%y-%m.log";
|
||||||
|
|
||||||
|
# other choices are below, or you can make your own -- but PLEASE MAKE SURE
|
||||||
|
# the directory exists and is writable; gitolite won't do that for you (unless
|
||||||
|
# it is the default, which is "$GL_ADMINDIR/logs")
|
||||||
|
|
||||||
|
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y-%m-%d.log";
|
||||||
|
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y.log";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# Please DO NOT change these three paths
|
||||||
|
|
||||||
|
$GL_CONF="$GL_ADMINDIR/conf/gitolite.conf";
|
||||||
|
$GL_KEYDIR="$GL_ADMINDIR/keydir";
|
||||||
|
$GL_CONF_COMPILED="$GL_ADMINDIR/conf/gitolite.conf-compiled.pm";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# if git on your server is on a standard path (that is
|
||||||
|
# ssh git@server git --version
|
||||||
|
# works), leave this setting as is. Otherwise, choose one of the
|
||||||
|
# alternatives, or write your own
|
||||||
|
|
||||||
|
$GIT_PATH="";
|
||||||
|
# $GIT_PATH="/opt/bin/";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# BIG CONFIG SETTINGS
|
||||||
|
|
||||||
|
# Please read doc/big-config.mkd for details
|
||||||
|
|
||||||
|
$GL_BIG_CONFIG = 1;
|
||||||
|
$GL_NO_DAEMON_NO_GITWEB = 1;
|
||||||
|
$GL_NO_CREATE_REPOS = 1;
|
||||||
|
$GL_NO_SETUP_AUTHKEYS = 1;
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# SECURITY SENSITIVE SETTINGS
|
||||||
|
#
|
||||||
|
# Settings below this point may have security implications. That
|
||||||
|
# usually means that I have not thought hard enough about all the
|
||||||
|
# possible ways to crack security if these settings are enabled.
|
||||||
|
|
||||||
|
# Please see details on each setting for specifics, if any.
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# ALLOW REPO ADMIN TO SET GITCONFIG KEYS
|
||||||
|
#
|
||||||
|
# Gitolite allows you to set git repo options using the "config" keyword; see
|
||||||
|
# conf/example.conf for details and syntax.
|
||||||
|
#
|
||||||
|
# However, if you are in an installation where the repo admin does not (and
|
||||||
|
# should not) have shell access to the server, then allowing him to set
|
||||||
|
# arbitrary repo config options *may* be a security risk -- some config
|
||||||
|
# settings may allow executing arbitrary commands.
|
||||||
|
#
|
||||||
|
# You have 3 choices. By default $GL_GITCONFIG_KEYS is left empty, which
|
||||||
|
# completely disables this feature (meaning you cannot set git configs from
|
||||||
|
# the repo config).
|
||||||
|
|
||||||
|
$GL_GITCONFIG_KEYS = "";
|
||||||
|
|
||||||
|
# The second choice is to give it a space separated list of settings you
|
||||||
|
# consider safe. (These are actually treated as a set of regular expression
|
||||||
|
# patterns, and any one of them must match). For example:
|
||||||
|
# $GL_GITCONFIG_KEYS = "core\.logAllRefUpdates core\..*compression";
|
||||||
|
# allows repo admins to set one of those 3 config keys (yes, that second
|
||||||
|
# pattern matches two settings from "man git-config", if you look)
|
||||||
|
#
|
||||||
|
# The third choice (which you may have guessed already if you're familiar with
|
||||||
|
# regular expressions) is to allow anything and everything:
|
||||||
|
# $GL_GITCONFIG_KEYS = ".*";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# EXTERNAL COMMAND HELPER -- HTPASSWD
|
||||||
|
|
||||||
|
# security note: runs an external command (htpasswd) with specific arguments,
|
||||||
|
# including a user-chosen "password".
|
||||||
|
|
||||||
|
# if you want to enable the "htpasswd" command, give this the absolute path to
|
||||||
|
# whatever file apache (etc) expect to find the passwords in.
|
||||||
|
|
||||||
|
$HTPASSWD_FILE = "";
|
||||||
|
|
||||||
|
# Look in doc/3 ("easier to link gitweb authorisation with gitolite" section)
|
||||||
|
# for more details on using this feature.
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# EXTERNAL COMMAND HELPER -- RSYNC
|
||||||
|
|
||||||
|
# security note: runs an external command (rsync) with specific arguments, all
|
||||||
|
# presumably filled in correctly by the client-side rsync.
|
||||||
|
|
||||||
|
# base path of all the files that are accessible via rsync. Must be an
|
||||||
|
# absolute path. Leave it undefined or set to the empty string to disable the
|
||||||
|
# rsync helper.
|
||||||
|
|
||||||
|
$RSYNC_BASE = "";
|
||||||
|
|
||||||
|
# $RSYNC_BASE = "/home/git/up-down";
|
||||||
|
# $RSYNC_BASE = "/tmp/up-down";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# EXTERNAL COMMAND HELPER -- SVNSERVE
|
||||||
|
|
||||||
|
# security note: runs an external command (svnserve) with specific arguments,
|
||||||
|
# as specified below. %u is substituted with the username.
|
||||||
|
|
||||||
|
# This setting allows launching svnserve when requested by the ssh client.
|
||||||
|
# This allows using the same SSH setup (hostname/username/public key) for both
|
||||||
|
# SVN and git access. Leave it undefined or set to the empty string to disable
|
||||||
|
# svnserve access.
|
||||||
|
|
||||||
|
$SVNSERVE = "";
|
||||||
|
# $SVNSERVE = "/usr/bin/svnserve -r /var/svn/ -t --tunnel-user=%u";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# ALLOW REPO CONFIG TO USE WILDCARDS
|
||||||
|
|
||||||
|
# security note: this used to in a separate "wildrepos" branch. You can
|
||||||
|
# create repositories based on wild cards, give "ownership" to the specific
|
||||||
|
# user who created it, allow him/her to hand out R and RW permissions to other
|
||||||
|
# users to collaborate, etc. This is powerful stuff, and I've made it as
|
||||||
|
# secure as I can, but it hasn't had the kind of rigorous line-by-line
|
||||||
|
# analysis that the old "master" branch had.
|
||||||
|
|
||||||
|
# This has now been rolled into master, with all the functionality gated by
|
||||||
|
# this variable. Set this to 1 if you want to enable the wildrepos features.
|
||||||
|
# Please see doc/4-wildcard-repositories.mkd for details.
|
||||||
|
|
||||||
|
$GL_WILDREPOS = 0;
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# DEFAULT WILDCARD PERMISSIONS
|
||||||
|
|
||||||
|
# If set, this value will be used as the default user-level permission rule of
|
||||||
|
# new wildcard repositories. The user can change this value with the setperms command
|
||||||
|
# as desired after repository creation; it is only a default. Note that @all can be
|
||||||
|
# used here but is special; no other groups can be used in user-level permissions.
|
||||||
|
|
||||||
|
# $GL_WILDREPOS_DEFPERMS = 'R = @all';
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# HOOK CHAINING
|
||||||
|
|
||||||
|
# by default, the update hook in every repo chains to "update.secondary".
|
||||||
|
# Similarly, the post-update hook in the admin repo chains to
|
||||||
|
# "post-update.secondary". If you're fine with the defaults, there's no need
|
||||||
|
# to do anything here. However, if you want to use different names or paths,
|
||||||
|
# change these variables
|
||||||
|
|
||||||
|
# $UPDATE_CHAINS_TO = "hooks/update.secondary";
|
||||||
|
# $ADMIN_POST_UPDATE_CHAINS_TO = "hooks/post-update.secondary";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# ADMIN DEFINED COMMANDS
|
||||||
|
|
||||||
|
# WARNING: Use this feature only if (a) you really really know what you're
|
||||||
|
# doing or (b) you really don't care too much about security. Please read
|
||||||
|
# doc/admin-defined-commands.mkd for details.
|
||||||
|
|
||||||
|
# $GL_ADC_PATH = "";
|
||||||
|
|
||||||
|
# --------------------------------------
|
||||||
|
# per perl rules, this should be the last line in such a file:
|
||||||
|
1;
|
||||||
|
|
||||||
|
# Local variables:
|
||||||
|
# mode: perl
|
||||||
|
# End:
|
||||||
|
# vim: set syn=perl:
|
195
files/gitolite3.rc
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
# configuration variables for gitolite
|
||||||
|
|
||||||
|
# This file is in perl syntax. But you do NOT need to know perl to edit it --
|
||||||
|
# just mind the commas, use single quotes unless you know what you're doing,
|
||||||
|
# and make sure the brackets and braces stay matched up!
|
||||||
|
|
||||||
|
# (Tip: perl allows a comma after the last item in a list also!)
|
||||||
|
|
||||||
|
# HELP for commands can be had by running the command with "-h".
|
||||||
|
|
||||||
|
# HELP for all the other FEATURES can be found in the documentation (look for
|
||||||
|
# "list of non-core programs shipped with gitolite" in the master index) or
|
||||||
|
# directly in the corresponding source file.
|
||||||
|
|
||||||
|
%RC = (
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
# default umask gives you perms of '0700'; see the rc file docs for
|
||||||
|
# how/why you might change this
|
||||||
|
UMASK => 0077,
|
||||||
|
|
||||||
|
# look for "git-config" in the documentation
|
||||||
|
GIT_CONFIG_KEYS => '',
|
||||||
|
|
||||||
|
# comment out if you don't need all the extra detail in the logfile
|
||||||
|
LOG_EXTRA => 1,
|
||||||
|
# syslog options
|
||||||
|
# 1. leave this section as is for normal gitolite logging
|
||||||
|
# 2. uncomment this line to log only to syslog:
|
||||||
|
# LOG_DEST => 'syslog',
|
||||||
|
# 3. uncomment this line to log to syslog and the normal gitolite log:
|
||||||
|
# LOG_DEST => 'syslog,normal',
|
||||||
|
|
||||||
|
# roles. add more roles (like MANAGER, TESTER, ...) here.
|
||||||
|
# WARNING: if you make changes to this hash, you MUST run 'gitolite
|
||||||
|
# compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
|
||||||
|
ROLES => {
|
||||||
|
READERS => 1,
|
||||||
|
WRITERS => 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
# enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!!
|
||||||
|
# CACHE => 'Redis',
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
# rc variables used by various features
|
||||||
|
|
||||||
|
# the 'info' command prints this as additional info, if it is set
|
||||||
|
# SITE_INFO => 'Please see http://blahblah/gitolite for more help',
|
||||||
|
|
||||||
|
# the CpuTime feature uses these
|
||||||
|
# display user, system, and elapsed times to user after each git operation
|
||||||
|
# DISPLAY_CPU_TIME => 1,
|
||||||
|
# display a warning if total CPU times (u, s, cu, cs) crosses this limit
|
||||||
|
# CPU_TIME_WARN_LIMIT => 0.1,
|
||||||
|
|
||||||
|
# the Mirroring feature needs this
|
||||||
|
# HOSTNAME => "foo",
|
||||||
|
|
||||||
|
# TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING!
|
||||||
|
# CACHE_TTL => 600,
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
# suggested locations for site-local gitolite code (see cust.html)
|
||||||
|
|
||||||
|
# this one is managed directly on the server
|
||||||
|
# LOCAL_CODE => "$ENV{HOME}/local",
|
||||||
|
|
||||||
|
# or you can use this, which lets you put everything in a subdirectory
|
||||||
|
# called "local" in your gitolite-admin repo. For a SECURITY WARNING
|
||||||
|
# on this, see http://gitolite.com/gitolite/non-core.html#pushcode
|
||||||
|
# LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
# List of commands and features to enable
|
||||||
|
|
||||||
|
ENABLE => [
|
||||||
|
|
||||||
|
# COMMANDS
|
||||||
|
|
||||||
|
# These are the commands enabled by default
|
||||||
|
'help',
|
||||||
|
'desc',
|
||||||
|
'info',
|
||||||
|
'perms',
|
||||||
|
'writable',
|
||||||
|
|
||||||
|
# Uncomment or add new commands here.
|
||||||
|
# 'create',
|
||||||
|
# 'fork',
|
||||||
|
# 'mirror',
|
||||||
|
# 'readme',
|
||||||
|
# 'sskm',
|
||||||
|
# 'D',
|
||||||
|
|
||||||
|
# These FEATURES are enabled by default.
|
||||||
|
|
||||||
|
# essential (unless you're using smart-http mode)
|
||||||
|
'ssh-authkeys',
|
||||||
|
|
||||||
|
# creates git-config enties from gitolite.conf file entries like 'config foo.bar = baz'
|
||||||
|
'git-config',
|
||||||
|
|
||||||
|
# creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
|
||||||
|
'daemon',
|
||||||
|
|
||||||
|
# creates projects.list file; if you don't use gitweb, comment this out
|
||||||
|
#'gitweb',
|
||||||
|
|
||||||
|
# These FEATURES are disabled by default; uncomment to enable. If you
|
||||||
|
# need to add new ones, ask on the mailing list :-)
|
||||||
|
|
||||||
|
# user-visible behaviour
|
||||||
|
|
||||||
|
# prevent wild repos auto-create on fetch/clone
|
||||||
|
# 'no-create-on-read',
|
||||||
|
# no auto-create at all (don't forget to enable the 'create' command!)
|
||||||
|
# 'no-auto-create',
|
||||||
|
|
||||||
|
# access a repo by another (possibly legacy) name
|
||||||
|
# 'Alias',
|
||||||
|
|
||||||
|
# give some users direct shell access. See documentation in
|
||||||
|
# sts.html for details on the following two choices.
|
||||||
|
# "Shell $ENV{HOME}/.gitolite.shell-users",
|
||||||
|
# 'Shell alice bob',
|
||||||
|
|
||||||
|
# set default roles from lines like 'option default.roles-1 = ...', etc.
|
||||||
|
# 'set-default-roles',
|
||||||
|
|
||||||
|
# show more detailed messages on deny
|
||||||
|
# 'expand-deny-messages',
|
||||||
|
|
||||||
|
# show a message of the day
|
||||||
|
# 'Motd',
|
||||||
|
|
||||||
|
# system admin stuff
|
||||||
|
|
||||||
|
# enable mirroring (don't forget to set the HOSTNAME too!)
|
||||||
|
# 'Mirroring',
|
||||||
|
|
||||||
|
# allow people to submit pub files with more than one key in them
|
||||||
|
# 'ssh-authkeys-split',
|
||||||
|
|
||||||
|
# selective read control hack
|
||||||
|
# 'partial-copy',
|
||||||
|
|
||||||
|
# manage local, gitolite-controlled, copies of read-only upstream repos
|
||||||
|
# 'upstream',
|
||||||
|
|
||||||
|
# updates 'description' file instead of 'gitweb.description' config item
|
||||||
|
# 'cgit',
|
||||||
|
|
||||||
|
# allow repo-specific hooks to be added
|
||||||
|
# 'repo-specific-hooks',
|
||||||
|
|
||||||
|
# performance, logging, monitoring...
|
||||||
|
|
||||||
|
# be nice
|
||||||
|
# 'renice 10',
|
||||||
|
|
||||||
|
# log CPU times (user, system, cumulative user, cumulative system)
|
||||||
|
# 'CpuTime',
|
||||||
|
|
||||||
|
# syntactic_sugar for gitolite.conf and included files
|
||||||
|
|
||||||
|
# allow backslash-escaped continuation lines in gitolite.conf
|
||||||
|
# 'continuation-lines',
|
||||||
|
|
||||||
|
# create implicit user groups from directory names in keydir/
|
||||||
|
# 'keysubdirs-as-groups',
|
||||||
|
|
||||||
|
# allow simple line-oriented macros
|
||||||
|
# 'macros',
|
||||||
|
|
||||||
|
# Kindergarten mode
|
||||||
|
|
||||||
|
# disallow various things that sensible people shouldn't be doing anyway
|
||||||
|
# 'Kindergarten',
|
||||||
|
],
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# per perl rules, this should be the last line in such a file:
|
||||||
|
1;
|
||||||
|
|
||||||
|
# Local variables:
|
||||||
|
# mode: perl
|
||||||
|
# End:
|
||||||
|
# vim: set syn=perl:
|
118
files/load_from_disk.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||||
|
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
|
||||||
|
|
||||||
|
def get_poc_of_pkgs(debug=False):
|
||||||
|
""" Retrieve a dictionary giving the point of contact of each package
|
||||||
|
in pkgdb.
|
||||||
|
"""
|
||||||
|
if debug:
|
||||||
|
print 'Querying pkgdb'
|
||||||
|
PKGDB_URL = 'https://admin.fedoraproject.org/pkgdb/api/'
|
||||||
|
req = requests.get(PKGDB_URL + 'bugzilla').text
|
||||||
|
if debug:
|
||||||
|
print 'Pkgdb data retrieved, getting POC'
|
||||||
|
pkgs = {}
|
||||||
|
for line in req.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
line = line.split('|')
|
||||||
|
if len(line) < 4:
|
||||||
|
continue
|
||||||
|
pkgs[line[1]] = line[3]
|
||||||
|
|
||||||
|
return pkgs
|
||||||
|
|
||||||
|
|
||||||
|
def main(folder, debug=False):
|
||||||
|
"""
|
||||||
|
Logic:
|
||||||
|
- Query the list of maintainer/PoC from pkgdb
|
||||||
|
- Browse the directory
|
||||||
|
- For each git in the directory, create the project with the correct POC
|
||||||
|
"""
|
||||||
|
pocs = get_poc_of_pkgs(debug=debug)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print 'Adding the user to the DB'
|
||||||
|
for user in sorted(set(pocs.values())):
|
||||||
|
if debug:
|
||||||
|
print user
|
||||||
|
try:
|
||||||
|
pagure.lib.set_up_user(
|
||||||
|
session=pagure.SESSION,
|
||||||
|
username=user,
|
||||||
|
fullname=user,
|
||||||
|
default_email='%s@fedoraproject.org' % user,
|
||||||
|
keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None),
|
||||||
|
)
|
||||||
|
pagure.SESSION.commit()
|
||||||
|
except SQLAlchemyError as err:
|
||||||
|
pagure.SESSION.rollback()
|
||||||
|
print 'ERROR with user %s' % user
|
||||||
|
print err
|
||||||
|
|
||||||
|
for project in sorted(os.listdir(folder)):
|
||||||
|
if debug:
|
||||||
|
print project
|
||||||
|
|
||||||
|
if not project.endswith('.git'):
|
||||||
|
if debug:
|
||||||
|
print ' -skip: not a git repository'
|
||||||
|
continue
|
||||||
|
|
||||||
|
if project.split('.git')[0] not in pocs:
|
||||||
|
if debug:
|
||||||
|
print ' -skip: no pocs'
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
name = project.split('.git')[0]
|
||||||
|
pagure.lib.new_project(
|
||||||
|
session=pagure.SESSION,
|
||||||
|
user=pocs[name],
|
||||||
|
name=name,
|
||||||
|
blacklist=pagure.APP.config['BLACKLISTED_PROJECTS'],
|
||||||
|
gitfolder=pagure.APP.config['GIT_FOLDER'],
|
||||||
|
docfolder=pagure.APP.config['DOCS_FOLDER'],
|
||||||
|
ticketfolder=pagure.APP.config['TICKETS_FOLDER'],
|
||||||
|
requestfolder=pagure.APP.config['REQUESTS_FOLDER'],
|
||||||
|
)
|
||||||
|
pagure.SESSION.commit()
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
print 'ERROR with project %s' % project
|
||||||
|
print err
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
pagure.SESSION.rollback()
|
||||||
|
print 'ERROR (DB) with project %s' % project
|
||||||
|
print err
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Script creating projects on pagure based on the git '
|
||||||
|
'repos present in the specified folder and the pkgdb information.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'folder',
|
||||||
|
help='Folder containing all the git repos of the projects to create')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug', dest='debug', action='store_true', default=False,
|
||||||
|
help='Print the debugging output')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
main(args.folder, debug=args.debug)
|
214
files/pagure.cfg.sample
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
### Set the time after which the admin session expires
|
||||||
|
# There are two sessions on pagure, login that holds for 31 days and
|
||||||
|
# the session defined here after which an user has to re-login.
|
||||||
|
# This session is used when accessing all administrative parts of pagure
|
||||||
|
# (ie: changing a project's or a user's settings)
|
||||||
|
ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
|
||||||
|
|
||||||
|
### Secret key for the Flask application
|
||||||
|
SECRET_KEY='<The web application secret key>'
|
||||||
|
|
||||||
|
### url to the database server:
|
||||||
|
#DB_URL=mysql://user:pass@host/db_name
|
||||||
|
#DB_URL=postgres://user:pass@host/db_name
|
||||||
|
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||||
|
|
||||||
|
### The FAS group in which the admin of pagure are
|
||||||
|
ADMIN_GROUP = ['sysadmin-main']
|
||||||
|
|
||||||
|
### Hard-coded list of global admins
|
||||||
|
PAGURE_ADMIN_USERS = []
|
||||||
|
|
||||||
|
### The email address to which the flask.log will send the errors (tracebacks)
|
||||||
|
EMAIL_ERROR = 'pingou@pingoured.fr'
|
||||||
|
|
||||||
|
### SMTP settings
|
||||||
|
SMTP_SERVER = 'localhost'
|
||||||
|
SMTP_PORT = 25
|
||||||
|
SMTP_SSL = False
|
||||||
|
|
||||||
|
#Specify both for enabling SMTP with auth
|
||||||
|
SMTP_USERNAME = None
|
||||||
|
SMTP_PASSWORD = None
|
||||||
|
|
||||||
|
### Information used to sent notifications
|
||||||
|
FROM_EMAIL = 'pagure@pagure.io'
|
||||||
|
DOMAIN_EMAIL_NOTIFICATIONS = 'pagure.io'
|
||||||
|
SALT_EMAIL = '<secret key to be changed>'
|
||||||
|
|
||||||
|
### The URL at which the project is available.
|
||||||
|
APP_URL = 'https://pagure.io/'
|
||||||
|
### The URL at which the documentation of projects will be available
|
||||||
|
## This should be in a different domain to avoid XSS issues since we want
|
||||||
|
## to allow raw html to be displayed (different domain, ie not a sub-domain).
|
||||||
|
DOC_APP_URL = 'https://docs.pagure.org'
|
||||||
|
|
||||||
|
### The URL to use to clone git repositories.
|
||||||
|
GIT_URL_SSH = 'ssh://git@pagure.io/'
|
||||||
|
GIT_URL_GIT = 'git://pagure.io/'
|
||||||
|
|
||||||
|
### Folder containing to the git repos
|
||||||
|
GIT_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'repos'
|
||||||
|
)
|
||||||
|
|
||||||
|
### Folder containing the forks repos
|
||||||
|
FORK_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'forks'
|
||||||
|
)
|
||||||
|
|
||||||
|
### Folder containing the docs repos
|
||||||
|
DOCS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'docs'
|
||||||
|
)
|
||||||
|
|
||||||
|
### Folder containing the tickets repos
|
||||||
|
TICKETS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'tickets'
|
||||||
|
)
|
||||||
|
|
||||||
|
### Folder containing the pull-requests repos
|
||||||
|
REQUESTS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'requests'
|
||||||
|
)
|
||||||
|
|
||||||
|
### Folder containing the clones for the remote pull-requests
|
||||||
|
REMOTE_GIT_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'remotes'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
### Configuration file for gitolite
|
||||||
|
GITOLITE_CONFIG = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'gitolite.conf'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
### Home folder of the gitolite user
|
||||||
|
### Folder where to run gl-compile-conf from
|
||||||
|
GITOLITE_HOME = None
|
||||||
|
|
||||||
|
### Version of gitolite used: 2 or 3?
|
||||||
|
GITOLITE_VERSION = 3
|
||||||
|
|
||||||
|
### Folder containing all the public ssh keys for gitolite
|
||||||
|
GITOLITE_KEYDIR = None
|
||||||
|
|
||||||
|
### Path to the gitolite.rc file
|
||||||
|
GL_RC = None
|
||||||
|
|
||||||
|
### Path to the /bin directory where the gitolite tools can be found
|
||||||
|
GL_BINDIR = None
|
||||||
|
|
||||||
|
|
||||||
|
# SSH Information
|
||||||
|
|
||||||
|
### The ssh certificates of the git server to be provided to the user
|
||||||
|
### /!\ format is important
|
||||||
|
# SSH_KEYS = {'RSA': {'fingerprint': '<foo>', 'pubkey': '<bar>'}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Optional configuration
|
||||||
|
|
||||||
|
### Number of items displayed per page
|
||||||
|
# Used when listing items
|
||||||
|
ITEM_PER_PAGE = 50
|
||||||
|
|
||||||
|
### Maximum size of the uploaded content
|
||||||
|
# Used to limit the size of file attached to a ticket for example
|
||||||
|
MAX_CONTENT_LENGTH = 4 * 1024 * 1024 # 4 megabytes
|
||||||
|
|
||||||
|
### Lenght for short commits ids or file hex
|
||||||
|
SHORT_LENGTH = 6
|
||||||
|
|
||||||
|
### List of blacklisted project names that can conflicts for pagure's URLs
|
||||||
|
### or other
|
||||||
|
BLACKLISTED_PROJECTS = [
|
||||||
|
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||||
|
'logout', 'login', 'users', 'groups', 'projects']
|
||||||
|
|
||||||
|
### IP addresses allowed to access the internal endpoints
|
||||||
|
### These endpoints are used by the milter and are security sensitive, thus
|
||||||
|
### the IP filter
|
||||||
|
IP_ALLOWED_INTERNAL = ['127.0.0.1', 'localhost', '::1']
|
||||||
|
|
||||||
|
### EventSource/Web-Hook/Redis configuration
|
||||||
|
# The eventsource integration is what allows pagure to refresh the content
|
||||||
|
# on your page when someone else comments on the ticket (and this without
|
||||||
|
# asking you to reload the page.
|
||||||
|
# By default it is off, ie: EVENTSOURCE_SOURCE is None, to turn it on, specify
|
||||||
|
# here what the URL of the eventsource server is, for example:
|
||||||
|
# https://ev.pagure.io or https://pagure.io:8080 or whatever you are using
|
||||||
|
# (Note: the urls sent to it start with a '/' so no need to add one yourself)
|
||||||
|
EVENTSOURCE_SOURCE = None
|
||||||
|
# Port where the event source server is running (maybe be the same port
|
||||||
|
# as the one specified in EVENTSOURCE_SOURCE or a different one if you
|
||||||
|
# have something running in front of the server such as apache or stunnel).
|
||||||
|
EVENTSOURCE_PORT = 8080
|
||||||
|
# If this port is specified, the event source server will run another server
|
||||||
|
# at this port and will provide information about the number of active
|
||||||
|
# connections running on the first (main) event source server
|
||||||
|
#EV_STATS_PORT = 8888
|
||||||
|
# Web-hook can be turned on or off allowing using them for notifications, or
|
||||||
|
# not.
|
||||||
|
WEBHOOK = False
|
||||||
|
|
||||||
|
### Redis configuration
|
||||||
|
# A redis server is required for both the Event-Source server or the web-hook
|
||||||
|
# server.
|
||||||
|
REDIS_HOST = '0.0.0.0'
|
||||||
|
REDIS_PORT = 6379
|
||||||
|
REDIS_DB = 0
|
||||||
|
|
||||||
|
# Authentication related configuration option
|
||||||
|
|
||||||
|
### Switch the authentication method
|
||||||
|
# Specify which authentication method to use, defaults to `fas` can be or
|
||||||
|
# `local`
|
||||||
|
# Default: ``fas``.
|
||||||
|
PAGURE_AUTH = 'fas'
|
||||||
|
|
||||||
|
# When this is set to True, the session cookie will only be returned to the
|
||||||
|
# server via ssl (https). If you connect to the server via plain http, the
|
||||||
|
# cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||||
|
# This may be set to False when testing your application but should always
|
||||||
|
# be set to True in production.
|
||||||
|
# Default: ``True``.
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
|
||||||
|
# The name of the cookie used to store the session id.
|
||||||
|
# Default: ``.pagure``.
|
||||||
|
SESSION_COOKIE_NAME = 'pagure'
|
||||||
|
|
||||||
|
# Boolean specifying whether to check the user's IP address when retrieving
|
||||||
|
# its session. This make things more secure (thus is on by default) but
|
||||||
|
# under certain setup it might not work (for example is there are proxies
|
||||||
|
# in front of the application).
|
||||||
|
CHECK_SESSION_IP = True
|
||||||
|
|
||||||
|
# Used by SESSION_COOKIE_PATH
|
||||||
|
APPLICATION_ROOT = '/'
|
||||||
|
|
||||||
|
# Allow the backward compatiblity endpoints for the old URLs schema to
|
||||||
|
# see the commits of a repo. This is only interesting if you pagure instance
|
||||||
|
# was running since before version 1.3 and if you care about backward
|
||||||
|
# compatibility in your URLs.
|
||||||
|
OLD_VIEW_COMMIT_ENABLED = False
|
114
files/pagure.conf
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#WSGISocketPrefix run/wsgi
|
||||||
|
##WSGIRestrictStdout On
|
||||||
|
#WSGIRestrictSignal Off
|
||||||
|
#WSGIPythonOptimize 1
|
||||||
|
#WSGIPassAuthorization On
|
||||||
|
#WSGIDaemonProcess pagure user=git group=git maximum-requests=1000 display-name=pagure processes=4 threads=4 inactivity-timeout=300
|
||||||
|
## It is important that the doc server runs in a different apache process
|
||||||
|
#WSGIDaemonProcess paguredocs user=git group=git maximum-requests=1000 display-name=pagure processes=4 threads=4 inactivity-timeout=300
|
||||||
|
|
||||||
|
#<VirtualHost *:80>
|
||||||
|
#ServerName pagure.io
|
||||||
|
#Redirect permanent / https://pagure.io/
|
||||||
|
#</VirtualHost>
|
||||||
|
|
||||||
|
|
||||||
|
#<VirtualHost *:80>
|
||||||
|
#ServerName docs.pagure.org
|
||||||
|
#Redirect permanent / https://docs.pagure.org/
|
||||||
|
#</VirtualHost>
|
||||||
|
|
||||||
|
|
||||||
|
#<VirtualHost *:443>
|
||||||
|
#ServerName docs.pagure.org
|
||||||
|
|
||||||
|
#WSGIScriptAlias / /usr/share/pagure/docs_pagure.wsgi
|
||||||
|
|
||||||
|
#SSLEngine on
|
||||||
|
#SSLProtocol all -SSLv2 -SSLv3
|
||||||
|
## Use secure TLSv1.1 and TLSv1.2 ciphers
|
||||||
|
#Header always add Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
|
||||||
|
|
||||||
|
#SSLCertificateFile /etc/pki/tls/....crt
|
||||||
|
#SSLCertificateChainFile /etc/pki/tls/....intermediate.crt
|
||||||
|
#SSLCertificateKeyFile /etc/pki/tls/....key
|
||||||
|
|
||||||
|
#Alias /static /usr/lib/python2.7/site-packages/pagure/static/
|
||||||
|
|
||||||
|
#<Location />
|
||||||
|
#WSGIProcessGroup paguredocs
|
||||||
|
#<IfModule mod_authz_core.c>
|
||||||
|
## Apache 2.4
|
||||||
|
#Require all granted
|
||||||
|
#</IfModule>
|
||||||
|
#<IfModule !mod_authz_core.c>
|
||||||
|
## Apache 2.2
|
||||||
|
#Order deny,allow
|
||||||
|
#Allow from all
|
||||||
|
#</IfModule>
|
||||||
|
#</Location>
|
||||||
|
#</VirtualHost>
|
||||||
|
|
||||||
|
|
||||||
|
#<VirtualHost *:443>
|
||||||
|
#ServerName pagure.io
|
||||||
|
|
||||||
|
#WSGIScriptAlias / /usr/share/pagure/pagure.wsgi
|
||||||
|
|
||||||
|
#SSLEngine on
|
||||||
|
#SSLProtocol all -SSLv2 -SSLv3
|
||||||
|
## Use secure TLSv1.1 and TLSv1.2 ciphers
|
||||||
|
#Header always add Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
|
||||||
|
|
||||||
|
#SSLCertificateFile /etc/pki/tls/....crt
|
||||||
|
#SSLCertificateChainFile /etc/pki/tls/....intermediate.crt
|
||||||
|
#SSLCertificateKeyFile /etc/pki/tls/....key
|
||||||
|
|
||||||
|
#Alias /static /usr/lib/python2.7/site-packages/pagure/static/
|
||||||
|
#Alias /releases /var/www/releases
|
||||||
|
|
||||||
|
## Section used to support cloning git repo over http (https in this case)
|
||||||
|
#SetEnv GIT_PROJECT_ROOT /srv/git/repositories
|
||||||
|
|
||||||
|
#AliasMatch ^/(.*/objects/[0-9a-f]{2}/[0-9a-f]{38})$ /path/to/git/repositories/$1
|
||||||
|
#AliasMatch ^/(.*/objects/pack/pack-[0-9a-f]{40}.(pack|idx))$ /path/to/git/repositories/$1
|
||||||
|
#ScriptAliasMatch \
|
||||||
|
#"(?x)^/(.*/(HEAD | \
|
||||||
|
#info/refs | \
|
||||||
|
#objects/info/[^/]+ | \
|
||||||
|
#git-(upload|receive)-pack))$" \
|
||||||
|
#/usr/libexec/git-core/git-http-backend/$1
|
||||||
|
|
||||||
|
#<Location />
|
||||||
|
#WSGIProcessGroup pagure
|
||||||
|
#<IfModule mod_authz_core.c>
|
||||||
|
## Apache 2.4
|
||||||
|
#Require all granted
|
||||||
|
#</IfModule>
|
||||||
|
#<IfModule !mod_authz_core.c>
|
||||||
|
## Apache 2.2
|
||||||
|
#Order deny,allow
|
||||||
|
#Allow from all
|
||||||
|
#</IfModule>
|
||||||
|
#</Location>
|
||||||
|
|
||||||
|
## Folder where are stored the tarball of the releases
|
||||||
|
#<Location /releases>
|
||||||
|
#WSGIProcessGroup pagure
|
||||||
|
#<IfModule mod_authz_core.c>
|
||||||
|
## Apache 2.4
|
||||||
|
#Require all granted
|
||||||
|
#</IfModule>
|
||||||
|
#<IfModule !mod_authz_core.c>
|
||||||
|
## Apache 2.2
|
||||||
|
#Order deny,allow
|
||||||
|
#Allow from all
|
||||||
|
#</IfModule>
|
||||||
|
#</Location>
|
||||||
|
|
||||||
|
#<Directory /var/www/releases>
|
||||||
|
#Options +Indexes
|
||||||
|
#</Directory>
|
||||||
|
|
||||||
|
#</VirtualHost>
|
||||||
|
|
1083
files/pagure.spec
Normal file
28
files/pagure.wsgi
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#-*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# The three lines below are required to run on EL6 as EL6 has
|
||||||
|
# two possible version of python-sqlalchemy and python-jinja2
|
||||||
|
# These lines make sure the application uses the correct version.
|
||||||
|
import __main__
|
||||||
|
__main__.__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
# Set the environment variable pointing to the configuration file
|
||||||
|
import os
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
# Set the environment variable if the tmp folder needs to be moved
|
||||||
|
# Might be necessary to work around bug in libgit2:
|
||||||
|
# refs: https://github.com/libgit2/libgit2/issues/2965
|
||||||
|
# and https://github.com/libgit2/libgit2/issues/2797
|
||||||
|
os.environ['TEMP'] = '/var/tmp/'
|
||||||
|
|
||||||
|
# The following is only needed if you did not install pagure
|
||||||
|
# as a python module (for example if you run it from a git clone).
|
||||||
|
#import sys
|
||||||
|
#sys.path.insert(0, '/path/to/pagure/')
|
||||||
|
|
||||||
|
|
||||||
|
# The most import line to make the wsgi working
|
||||||
|
from pagure import APP as application
|
||||||
|
#application.debug = True
|
247
milters/comment_email_milter.py
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Milter calls methods of your class at milter events.
|
||||||
|
# Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
|
||||||
|
# You can also add/del recipients, replacebody, add/del headers, etc.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import email
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import urlparse
|
||||||
|
import StringIO
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from socket import AF_INET, AF_INET6
|
||||||
|
from multiprocessing import Process as Thread, Queue
|
||||||
|
|
||||||
|
import Milter
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from Milter.utils import parse_addr
|
||||||
|
|
||||||
|
logq = Queue(maxsize=4)
|
||||||
|
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_body(emailobj):
|
||||||
|
''' Return the body of the email, preferably in text.
|
||||||
|
'''
|
||||||
|
body = None
|
||||||
|
if emailobj.is_multipart():
|
||||||
|
for payload in emailobj.get_payload():
|
||||||
|
body = payload.get_payload()
|
||||||
|
if payload.get_content_type() == 'text/plain':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
body = emailobj.get_payload()
|
||||||
|
|
||||||
|
enc = emailobj['Content-Transfer-Encoding']
|
||||||
|
if enc == 'base64':
|
||||||
|
body = base64.decodestring(body)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def clean_item(item):
|
||||||
|
''' For an item provided as <item> return the content, if there are no
|
||||||
|
<> then return the string.
|
||||||
|
'''
|
||||||
|
if '<' in item:
|
||||||
|
item = item.split('<')[1]
|
||||||
|
if '>' in item:
|
||||||
|
item = item.split('>')[0]
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
class PagureMilter(Milter.Base):
|
||||||
|
|
||||||
|
def __init__(self): # A new instance with each new connection.
|
||||||
|
self.id = Milter.uniqueID() # Integer incremented with each call.
|
||||||
|
self.fp = None
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
print(message)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def envfrom(self, mailfrom, *str):
|
||||||
|
self.log("mail from: %s - %s" % (mailfrom, str))
|
||||||
|
self.fromparms = Milter.dictfromlist(str)
|
||||||
|
# NOTE: self.fp is only an *internal* copy of message data. You
|
||||||
|
# must use addheader, chgheader, replacebody to change the message
|
||||||
|
# on the MTA.
|
||||||
|
self.fp = StringIO.StringIO()
|
||||||
|
self.canon_from = '@'.join(parse_addr(mailfrom))
|
||||||
|
self.fp.write('From %s %s\n' % (self.canon_from, time.ctime()))
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.noreply
|
||||||
|
def header(self, name, hval):
|
||||||
|
''' Headers '''
|
||||||
|
# add header to buffer
|
||||||
|
self.fp.write("%s: %s\n" % (name, hval))
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.noreply
|
||||||
|
def eoh(self):
|
||||||
|
''' End of Headers '''
|
||||||
|
self.fp.write("\n")
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.noreply
|
||||||
|
def body(self, chunk):
|
||||||
|
''' Body '''
|
||||||
|
self.fp.write(chunk)
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
@Milter.noreply
|
||||||
|
def envrcpt(self, to, *str):
|
||||||
|
rcptinfo = to, Milter.dictfromlist(str)
|
||||||
|
print rcptinfo
|
||||||
|
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
def eom(self):
|
||||||
|
''' End of Message '''
|
||||||
|
self.fp.seek(0)
|
||||||
|
msg = email.message_from_file(self.fp)
|
||||||
|
|
||||||
|
msg_id = msg.get('In-Reply-To', None)
|
||||||
|
if msg_id is None:
|
||||||
|
self.log('No In-Reply-To, keep going')
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
# Ensure we don't get extra lines in the message-id
|
||||||
|
msg_id = msg_id.split('\n')[0].strip()
|
||||||
|
|
||||||
|
self.log('msg-ig %s' % msg_id)
|
||||||
|
self.log('To %s' % msg['to'])
|
||||||
|
self.log('Cc %s' % msg.get('cc'))
|
||||||
|
self.log('From %s' % msg['From'])
|
||||||
|
|
||||||
|
# Ensure the user replied to his/her own notification, not that
|
||||||
|
# they are trying to forge their ID into someone else's
|
||||||
|
salt = pagure.APP.config.get('SALT_EMAIL')
|
||||||
|
m = hashlib.sha512('%s%s%s' % (msg_id, salt, clean_item(msg['From'])))
|
||||||
|
email_address = msg['to']
|
||||||
|
if 'reply+' in msg.get('cc', ''):
|
||||||
|
email_address = msg['cc']
|
||||||
|
if not 'reply+' in email_address:
|
||||||
|
self.log(
|
||||||
|
'No valid recipient email found in To/Cc: %s'
|
||||||
|
% email_address)
|
||||||
|
tohash = email_address.split('@')[0].split('+')[-1]
|
||||||
|
if m.hexdigest() != tohash:
|
||||||
|
self.log('hash: %s' % m.hexdigest())
|
||||||
|
self.log('tohash: %s' % tohash)
|
||||||
|
self.log('Hash does not correspond to the destination')
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
if msg['From'] and msg['From'] == pagure.APP.config.get('FROM_EMAIL'):
|
||||||
|
self.log("Let's not process the email we send")
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
msg_id = clean_item(msg_id)
|
||||||
|
|
||||||
|
if msg_id and '-ticket-' in msg_id:
|
||||||
|
self.log('Processing issue')
|
||||||
|
return self.handle_ticket_email(msg, msg_id)
|
||||||
|
elif msg_id and '-pull-request-' in msg_id:
|
||||||
|
self.log('Processing pull-request')
|
||||||
|
return self.handle_request_email(msg, msg_id)
|
||||||
|
else:
|
||||||
|
self.log('Not a pagure ticket or pull-request email, let it go')
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
|
||||||
|
def handle_ticket_email(self, emailobj, msg_id):
|
||||||
|
''' Add the email as a comment on a ticket. '''
|
||||||
|
uid = msg_id.split('-ticket-')[-1].split('@')[0]
|
||||||
|
parent_id = None
|
||||||
|
if '-' in uid:
|
||||||
|
uid, parent_id = uid.rsplit('-', 1)
|
||||||
|
if '/' in uid:
|
||||||
|
uid = uid.split('/')[0]
|
||||||
|
self.log('uid %s' % uid)
|
||||||
|
self.log('parent_id %s' % parent_id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'objid': uid,
|
||||||
|
'comment': get_email_body(emailobj),
|
||||||
|
'useremail': clean_item(emailobj['From']),
|
||||||
|
}
|
||||||
|
url = pagure.APP.config.get('APP_URL')
|
||||||
|
|
||||||
|
if url.endswith('/'):
|
||||||
|
url = url[:-1]
|
||||||
|
url = '%s/pv/ticket/comment/' % url
|
||||||
|
req = requests.put(url, data=data)
|
||||||
|
if req.status_code == 200:
|
||||||
|
self.log('Comment added')
|
||||||
|
return Milter.ACCEPT
|
||||||
|
self.log('Could not add the comment to pagure')
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
def handle_request_email(self, emailobj, msg_id):
|
||||||
|
''' Add the email as a comment on a request. '''
|
||||||
|
uid = msg_id.split('-pull-request-')[-1].split('@')[0]
|
||||||
|
parent_id = None
|
||||||
|
if '-' in uid:
|
||||||
|
uid, parent_id = uid.rsplit('-', 1)
|
||||||
|
if '/' in uid:
|
||||||
|
uid = uid.split('/')[0]
|
||||||
|
self.log('uid %s' % uid)
|
||||||
|
self.log('parent_id %s' % parent_id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'objid': uid,
|
||||||
|
'comment': get_email_body(emailobj),
|
||||||
|
'useremail': clean_item(emailobj['From']),
|
||||||
|
}
|
||||||
|
url = pagure.APP.config.get('APP_URL')
|
||||||
|
|
||||||
|
if url.endswith('/'):
|
||||||
|
url = url[:-1]
|
||||||
|
url = '%s/pv/pull-request/comment/' % url
|
||||||
|
req = requests.put(url, data=data)
|
||||||
|
|
||||||
|
return Milter.ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
def background():
|
||||||
|
while True:
|
||||||
|
t = logq.get()
|
||||||
|
if not t: break
|
||||||
|
msg,id,ts = t
|
||||||
|
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
|
||||||
|
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
|
||||||
|
for i in msg: print i,
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
bt = Thread(target=background)
|
||||||
|
bt.start()
|
||||||
|
socketname = "/var/run/pagure/paguresock"
|
||||||
|
timeout = 600
|
||||||
|
# Register to have the Milter factory create instances of your class:
|
||||||
|
Milter.factory = PagureMilter
|
||||||
|
print "%s pagure milter startup" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
|
sys.stdout.flush()
|
||||||
|
Milter.runmilter("paguremilter", socketname, timeout)
|
||||||
|
logq.put(None)
|
||||||
|
bt.join()
|
||||||
|
print "%s pagure milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
milters/milter_tempfile.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
d /var/run/pagure 0755 postfix postfix
|
14
milters/pagure_milter.service
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Pagure SMTP filter (Milter) Daemon (talk to postfix over a socket)
|
||||||
|
After=postfix.target
|
||||||
|
Documentation=https://github.com/pypingou/pagure
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python2 /usr/share/pagure/comment_email_milter.py
|
||||||
|
Type=simple
|
||||||
|
User=postfix
|
||||||
|
Group=postfix
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
11
pagure.egg-info/PKG-INFO
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Metadata-Version: 1.1
|
||||||
|
Name: pagure
|
||||||
|
Version: 2.3.4
|
||||||
|
Summary: A light-weight git-centered forge based on pygit2..
|
||||||
|
Home-page: https://fedorahosted.org/pagure/
|
||||||
|
Author: Pierre-Yves Chibon
|
||||||
|
Author-email: pingou@pingoured.fr
|
||||||
|
License: GPLv2+
|
||||||
|
Download-URL: https://fedorahosted.org/releases/p/r/pagure/
|
||||||
|
Description: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
340
pagure.egg-info/SOURCES.txt
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
LICENSE
|
||||||
|
MANIFEST.in
|
||||||
|
README.rst
|
||||||
|
UPGRADING.rst
|
||||||
|
createdb.py
|
||||||
|
requirements.txt
|
||||||
|
setup.py
|
||||||
|
alembic/env.py
|
||||||
|
alembic/script.py.mako
|
||||||
|
alembic/versions/15ea3c2cf83d_pr_comment_editing.py
|
||||||
|
alembic/versions/1b6d7dc5600a_versioning_passwords.py
|
||||||
|
alembic/versions/1cd0a853c697_add_closed_at_field_in_pr.py
|
||||||
|
alembic/versions/1f3de3853a1a_add_the_tree_id_column_to_pr_inline_.py
|
||||||
|
alembic/versions/22db0a833d35_add_notifications_to_tickets.py
|
||||||
|
alembic/versions/257a7ce22682_add_the_remote_git_entry.py
|
||||||
|
alembic/versions/298891e63039_change_the_status_of_pull_requests.py
|
||||||
|
alembic/versions/2aa7b3958bc5_add_the_milestones_column.py
|
||||||
|
alembic/versions/317a285e04a8_delete_hooks.py
|
||||||
|
alembic/versions/36116bb7a69b_add_the_url_field_to_project.py
|
||||||
|
alembic/versions/3b441ef4e928_comment_editing_issue.py
|
||||||
|
alembic/versions/3c25e14b855b_add_an_avatar_email_for_project.py
|
||||||
|
alembic/versions/43df5e588a87_add_closed_at_attribute_in_issues.py
|
||||||
|
alembic/versions/443e090da188_up_to_255_characters_for_project_name.py
|
||||||
|
alembic/versions/496f7a700f2e_add_priorities.py
|
||||||
|
alembic/versions/4cae55a80a42_add_the_initial_comment_on_the_pr_table.py
|
||||||
|
alembic/versions/58e60d869326_add_notification_bool_to_pr.py
|
||||||
|
alembic/versions/6190226bed0_add_the_updated_on_column_to_pull_.py
|
||||||
|
alembic/versions/abc71fd60fa_add_the_closed_by_column_to_pull_.py
|
||||||
|
alembic/versions/b5efae6bb23_add_merge_status_to_the_pull_requests_.py
|
||||||
|
doc/Makefile
|
||||||
|
doc/api.rst
|
||||||
|
doc/conf.py
|
||||||
|
doc/configuration.rst
|
||||||
|
doc/contributing.rst
|
||||||
|
doc/contributors.rst
|
||||||
|
doc/development.rst
|
||||||
|
doc/index.rst
|
||||||
|
doc/install.rst
|
||||||
|
doc/install_evs.rst
|
||||||
|
doc/install_milter.rst
|
||||||
|
doc/install_webhooks.rst
|
||||||
|
doc/milter.rst
|
||||||
|
doc/overview.ascii
|
||||||
|
doc/overview.rst
|
||||||
|
doc/overview_simple.ascii
|
||||||
|
doc/requirements.txt
|
||||||
|
doc/usage.rst
|
||||||
|
doc/_static/overview.png
|
||||||
|
doc/_static/overview_simple.png
|
||||||
|
doc/_static/pagure.png
|
||||||
|
doc/_static/site.css
|
||||||
|
doc/_templates/pagure-logo.html
|
||||||
|
doc/usage/first_steps.rst
|
||||||
|
doc/usage/pr_custom_page.rst
|
||||||
|
doc/usage/project_settings.rst
|
||||||
|
doc/usage/roadmap.rst
|
||||||
|
doc/usage/theming.rst
|
||||||
|
doc/usage/ticket_templates.rst
|
||||||
|
doc/usage/upgrade_db.rst
|
||||||
|
doc/usage/using_doc.rst
|
||||||
|
doc/usage/using_webhooks.rst
|
||||||
|
doc/usage/_static/pagure_custom_pr.png
|
||||||
|
doc/usage/_static/pagure_my_settings.png
|
||||||
|
doc/usage/_static/pagure_roadmap2.png
|
||||||
|
doc/usage/_static/pagure_ticket_template.png
|
||||||
|
ev-server/pagure-stream-server.py
|
||||||
|
ev-server/pagure_ev.service
|
||||||
|
files/alembic.ini
|
||||||
|
files/api_key_expire_mail.py
|
||||||
|
files/doc_pagure.wsgi
|
||||||
|
files/emoji_clean_json.py
|
||||||
|
files/gitolite.rc
|
||||||
|
files/gitolite3.rc
|
||||||
|
files/load_from_disk.py
|
||||||
|
files/pagure.cfg.sample
|
||||||
|
files/pagure.conf
|
||||||
|
files/pagure.spec
|
||||||
|
files/pagure.wsgi
|
||||||
|
milters/comment_email_milter.py
|
||||||
|
milters/milter_tempfile.conf
|
||||||
|
milters/pagure_milter.service
|
||||||
|
pagure/__init__.py
|
||||||
|
pagure/default_config.py
|
||||||
|
pagure/doc_utils.py
|
||||||
|
pagure/docs_server.py
|
||||||
|
pagure/exceptions.py
|
||||||
|
pagure/forms.py
|
||||||
|
pagure/login_forms.py
|
||||||
|
pagure/mail_logging.py
|
||||||
|
pagure/pfmarkdown.py
|
||||||
|
pagure/proxy.py
|
||||||
|
pagure.egg-info/PKG-INFO
|
||||||
|
pagure.egg-info/SOURCES.txt
|
||||||
|
pagure.egg-info/dependency_links.txt
|
||||||
|
pagure.egg-info/requires.txt
|
||||||
|
pagure.egg-info/top_level.txt
|
||||||
|
pagure/api/__init__.py
|
||||||
|
pagure/api/fork.py
|
||||||
|
pagure/api/issue.py
|
||||||
|
pagure/api/project.py
|
||||||
|
pagure/api/user.py
|
||||||
|
pagure/doc/api.rst
|
||||||
|
pagure/hooks/__init__.py
|
||||||
|
pagure/hooks/fedmsg.py
|
||||||
|
pagure/hooks/irc.py
|
||||||
|
pagure/hooks/mail.py
|
||||||
|
pagure/hooks/pagure_force_commit.py
|
||||||
|
pagure/hooks/pagure_hook.py
|
||||||
|
pagure/hooks/pagure_request_hook.py
|
||||||
|
pagure/hooks/pagure_ticket_hook.py
|
||||||
|
pagure/hooks/pagure_unsigned_commits.py
|
||||||
|
pagure/hooks/rtd.py
|
||||||
|
pagure/hooks/files/fedmsg_hook.py
|
||||||
|
pagure/hooks/files/git_multimail.py
|
||||||
|
pagure/hooks/files/pagure_block_unsigned.py
|
||||||
|
pagure/hooks/files/pagure_force_commit_hook.py
|
||||||
|
pagure/hooks/files/pagure_hook.py
|
||||||
|
pagure/hooks/files/pagure_hook_requests.py
|
||||||
|
pagure/hooks/files/pagure_hook_tickets.py
|
||||||
|
pagure/hooks/files/post-receive
|
||||||
|
pagure/hooks/files/pre-receive
|
||||||
|
pagure/hooks/files/rtd_hook.py
|
||||||
|
pagure/internal/__init__.py
|
||||||
|
pagure/lib/__init__.py
|
||||||
|
pagure/lib/git.py
|
||||||
|
pagure/lib/link.py
|
||||||
|
pagure/lib/login.py
|
||||||
|
pagure/lib/model.py
|
||||||
|
pagure/lib/notify.py
|
||||||
|
pagure/lib/repo.py
|
||||||
|
pagure/static/favicon.ico
|
||||||
|
pagure/static/issue_ev.js
|
||||||
|
pagure/static/jquery-1.10.2.js
|
||||||
|
pagure/static/jquery-ui-1.11.2.custom.min.js
|
||||||
|
pagure/static/jquery.dotdotdot.min.js
|
||||||
|
pagure/static/pagure-logo.png
|
||||||
|
pagure/static/pagure.css
|
||||||
|
pagure/static/request_ev.js
|
||||||
|
pagure/static/selectize.bootstrap3.css
|
||||||
|
pagure/static/selectize.min.js
|
||||||
|
pagure/static/stupidtable.min.js
|
||||||
|
pagure/static/toggle.css
|
||||||
|
pagure/static/upload.js
|
||||||
|
pagure/static/atwho/jquery.atwho.min.css
|
||||||
|
pagure/static/atwho/jquery.atwho.min.js
|
||||||
|
pagure/static/atwho/jquery.caret.min.js
|
||||||
|
pagure/static/codemirror/codemirror.css
|
||||||
|
pagure/static/codemirror/codemirror.js
|
||||||
|
pagure/static/codemirror/solarized.css
|
||||||
|
pagure/static/emoji/emoji_strategy.json
|
||||||
|
pagure/static/emoji/emojione.min.js
|
||||||
|
pagure/static/emoji/emojione.sprites.css
|
||||||
|
pagure/static/emoji/emojione.sprites.png
|
||||||
|
pagure/static/emoji/jquery.textcomplete.min.js
|
||||||
|
pagure/static/fonts/fonts.css
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.woff2
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.woff2
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.woff2
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.woff2
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.woff2
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.eot
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.svg
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.ttf
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.woff
|
||||||
|
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.woff2
|
||||||
|
pagure/static/hack_fonts/css/hack-extended.min.css
|
||||||
|
pagure/static/hack_fonts/fonts/eot/hack-bold-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/hack-bolditalic-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/hack-italic-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/hack-regular-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/latin/hack-bold-latin-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/latin/hack-bolditalic-latin-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/latin/hack-italic-latin-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/eot/latin/hack-regular-latin-webfont.eot
|
||||||
|
pagure/static/hack_fonts/fonts/svg/hack-bold-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/hack-bolditalic-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/hack-italic-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/hack-regular-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/latin/hack-bold-latin-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/latin/hack-bolditalic-latin-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/latin/hack-italic-latin-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/svg/latin/hack-regular-latin-webfont.svg
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/hack-bold-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/hack-bolditalic-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/hack-italic-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/hack-regular-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-bold-latin-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-bolditalic-latin-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-italic-latin-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-regular-latin-webfont.ttf
|
||||||
|
pagure/static/hack_fonts/fonts/woff/hack-bold-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/hack-bolditalic-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/hack-italic-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/hack-regular-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/latin/hack-bold-latin-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/latin/hack-bolditalic-latin-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/latin/hack-italic-latin-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff/latin/hack-regular-latin-webfont.woff
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/hack-bold-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/hack-bolditalic-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/hack-italic-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/hack-regular-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/latin/hack-bold-latin-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/latin/hack-bolditalic-latin-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/latin/hack-italic-latin-webfont.woff2
|
||||||
|
pagure/static/hack_fonts/fonts/woff2/latin/hack-regular-latin-webfont.woff2
|
||||||
|
pagure/static/images/link.png
|
||||||
|
pagure/static/images/spinner.gif
|
||||||
|
pagure/static/images/users.png
|
||||||
|
pagure/static/open_iconic_1.1.0/css/open-iconic.min.css
|
||||||
|
pagure/static/open_iconic_1.1.0/fonts/open-iconic.eot
|
||||||
|
pagure/static/open_iconic_1.1.0/fonts/open-iconic.otf
|
||||||
|
pagure/static/open_iconic_1.1.0/fonts/open-iconic.svg
|
||||||
|
pagure/static/open_iconic_1.1.0/fonts/open-iconic.ttf
|
||||||
|
pagure/static/open_iconic_1.1.0/fonts/open-iconic.woff
|
||||||
|
pagure/templates/_browseheader.html
|
||||||
|
pagure/templates/_formhelper.html
|
||||||
|
pagure/templates/_render_repo.html
|
||||||
|
pagure/templates/activity.html
|
||||||
|
pagure/templates/add_group.html
|
||||||
|
pagure/templates/add_group_project.html
|
||||||
|
pagure/templates/add_token.html
|
||||||
|
pagure/templates/add_user.html
|
||||||
|
pagure/templates/admin_index.html
|
||||||
|
pagure/templates/api.html
|
||||||
|
pagure/templates/comment_update.html
|
||||||
|
pagure/templates/commit.html
|
||||||
|
pagure/templates/commits.html
|
||||||
|
pagure/templates/doc_ssh_keys.html
|
||||||
|
pagure/templates/docs.html
|
||||||
|
pagure/templates/edit_file.html
|
||||||
|
pagure/templates/edit_tag.html
|
||||||
|
pagure/templates/fatal_error.html
|
||||||
|
pagure/templates/file.html
|
||||||
|
pagure/templates/forks.html
|
||||||
|
pagure/templates/group_info.html
|
||||||
|
pagure/templates/group_list.html
|
||||||
|
pagure/templates/index.html
|
||||||
|
pagure/templates/index_auth.html
|
||||||
|
pagure/templates/issue.html
|
||||||
|
pagure/templates/issues.html
|
||||||
|
pagure/templates/master.html
|
||||||
|
pagure/templates/new_issue.html
|
||||||
|
pagure/templates/new_project.html
|
||||||
|
pagure/templates/new_release.html
|
||||||
|
pagure/templates/not_found.html
|
||||||
|
pagure/templates/plugin.html
|
||||||
|
pagure/templates/pull_request.html
|
||||||
|
pagure/templates/pull_request_comment.html
|
||||||
|
pagure/templates/pull_request_title.html
|
||||||
|
pagure/templates/releases.html
|
||||||
|
pagure/templates/remote_pull_request.html
|
||||||
|
pagure/templates/repo_info.html
|
||||||
|
pagure/templates/repo_master.html
|
||||||
|
pagure/templates/requests.html
|
||||||
|
pagure/templates/roadmap.html
|
||||||
|
pagure/templates/settings.html
|
||||||
|
pagure/templates/unauthorized.html
|
||||||
|
pagure/templates/user_emails.html
|
||||||
|
pagure/templates/user_info.html
|
||||||
|
pagure/templates/user_list.html
|
||||||
|
pagure/templates/user_requests.html
|
||||||
|
pagure/templates/user_settings.html
|
||||||
|
pagure/templates/login/login.html
|
||||||
|
pagure/templates/login/password_change.html
|
||||||
|
pagure/templates/login/password_recover.html
|
||||||
|
pagure/templates/login/password_reset.html
|
||||||
|
pagure/templates/login/user_new.html
|
||||||
|
pagure/ui/__init__.py
|
||||||
|
pagure/ui/admin.py
|
||||||
|
pagure/ui/app.py
|
||||||
|
pagure/ui/filters.py
|
||||||
|
pagure/ui/fork.py
|
||||||
|
pagure/ui/groups.py
|
||||||
|
pagure/ui/issues.py
|
||||||
|
pagure/ui/login.py
|
||||||
|
pagure/ui/plugins.py
|
||||||
|
pagure/ui/repo.py
|
||||||
|
tests/__init__.py
|
||||||
|
tests/placebo.png
|
||||||
|
tests/test_config
|
||||||
|
tests/test_pagure_flask_api.py
|
||||||
|
tests/test_pagure_flask_api_auth.py
|
||||||
|
tests/test_pagure_flask_api_fork.py
|
||||||
|
tests/test_pagure_flask_api_issue.py
|
||||||
|
tests/test_pagure_flask_api_project.py
|
||||||
|
tests/test_pagure_flask_docs.py
|
||||||
|
tests/test_pagure_flask_dump_load_ticket.py
|
||||||
|
tests/test_pagure_flask_internal.py
|
||||||
|
tests/test_pagure_flask_ui_admin.py
|
||||||
|
tests/test_pagure_flask_ui_app.py
|
||||||
|
tests/test_pagure_flask_ui_fork.py
|
||||||
|
tests/test_pagure_flask_ui_groups.py
|
||||||
|
tests/test_pagure_flask_ui_issues.py
|
||||||
|
tests/test_pagure_flask_ui_login.py
|
||||||
|
tests/test_pagure_flask_ui_no_master_branch.py
|
||||||
|
tests/test_pagure_flask_ui_plugins.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_fedmsg.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_irc.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_mail.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_noff.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_pagure_hook.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_pagure_request_hook.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_pagure_ticket_hook.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_rtd_hook.py
|
||||||
|
tests/test_pagure_flask_ui_plugins_unsigned.py
|
||||||
|
tests/test_pagure_flask_ui_priorities.py
|
||||||
|
tests/test_pagure_flask_ui_repo.py
|
||||||
|
tests/test_pagure_flask_ui_repo_slash_name.py
|
||||||
|
tests/test_pagure_flask_ui_roadmap.py
|
||||||
|
tests/test_pagure_flask_ui_slash_branch_name.py
|
||||||
|
tests/test_pagure_lib.py
|
||||||
|
tests/test_pagure_lib_git.py
|
||||||
|
tests/test_pagure_lib_git_get_tags_objects.py
|
||||||
|
tests/test_pagure_lib_link.py
|
||||||
|
tests/test_pagure_lib_login.py
|
||||||
|
tests/test_pagure_lib_model.py
|
||||||
|
tests/test_zzz_pagure_flask_ui_old_commit.py
|
||||||
|
webhook-server/pagure-webhook-server.py
|
||||||
|
webhook-server/pagure_webhook.service
|
1
pagure.egg-info/dependency_links.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
31
pagure.egg-info/requires.txt
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
alembic
|
||||||
|
arrow
|
||||||
|
binaryornot
|
||||||
|
bleach
|
||||||
|
blinker
|
||||||
|
chardet
|
||||||
|
docutils
|
||||||
|
enum34
|
||||||
|
flask
|
||||||
|
flask-wtf
|
||||||
|
flask-multistatic
|
||||||
|
html5lib
|
||||||
|
kitchen
|
||||||
|
markdown
|
||||||
|
munch
|
||||||
|
Pillow
|
||||||
|
psutil
|
||||||
|
pygit2 >= 0.20.1
|
||||||
|
pygments
|
||||||
|
python-openid
|
||||||
|
python-openid-cla
|
||||||
|
python-openid-teams
|
||||||
|
redis
|
||||||
|
six
|
||||||
|
sqlalchemy >= 0.8
|
||||||
|
straight.plugin==1.4.0-post-1
|
||||||
|
trollius-redis
|
||||||
|
wtforms
|
||||||
|
python-fedora
|
||||||
|
cryptography
|
||||||
|
py-bcrypt
|
1
pagure.egg-info/top_level.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pagure
|
556
pagure/__init__.py
Normal file
|
@ -0,0 +1,556 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014-2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# These two lines are needed to run on EL6
|
||||||
|
__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
__version__ = '2.3.4'
|
||||||
|
__api_version__ = '0.7'
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import urlparse
|
||||||
|
from logging.handlers import SMTPHandler
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import pygit2
|
||||||
|
import werkzeug
|
||||||
|
from functools import wraps
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from pygments import highlight
|
||||||
|
from pygments.lexers.text import DiffLexer
|
||||||
|
from pygments.formatters import HtmlFormatter
|
||||||
|
|
||||||
|
from flask_multistatic import MultiStaticFlask
|
||||||
|
|
||||||
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
|
import pagure.exceptions
|
||||||
|
|
||||||
|
# Create the application.
|
||||||
|
APP = MultiStaticFlask(__name__)
|
||||||
|
APP.jinja_env.trim_blocks = True
|
||||||
|
APP.jinja_env.lstrip_blocks = True
|
||||||
|
|
||||||
|
# set up FAS
|
||||||
|
APP.config.from_object('pagure.default_config')
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' in os.environ:
|
||||||
|
APP.config.from_envvar('PAGURE_CONFIG')
|
||||||
|
|
||||||
|
|
||||||
|
if APP.config.get('THEME_TEMPLATE_FOLDER', False):
|
||||||
|
# Jinja can be told to look for templates in different folders
|
||||||
|
# That's what we do here
|
||||||
|
template_folder = APP.config['THEME_TEMPLATE_FOLDER']
|
||||||
|
if template_folder[0] != '/':
|
||||||
|
template_folder= os.path.join(
|
||||||
|
APP.root_path, APP.template_folder, template_folder)
|
||||||
|
import jinja2
|
||||||
|
# Jinja looks for the template in the order of the folders specified
|
||||||
|
templ_loaders = [
|
||||||
|
jinja2.FileSystemLoader(template_folder),
|
||||||
|
APP.jinja_loader,
|
||||||
|
]
|
||||||
|
APP.jinja_loader = jinja2.ChoiceLoader(templ_loaders)
|
||||||
|
|
||||||
|
|
||||||
|
if APP.config.get('THEME_STATIC_FOLDER', False):
|
||||||
|
static_folder = APP.config['THEME_STATIC_FOLDER']
|
||||||
|
if static_folder[0] != '/':
|
||||||
|
static_folder= os.path.join(
|
||||||
|
APP.root_path, 'static', static_folder)
|
||||||
|
# Unlike templates, to serve static files from multiples folders we
|
||||||
|
# need flask-multistatic
|
||||||
|
APP.static_folder = [
|
||||||
|
static_folder,
|
||||||
|
os.path.join(APP.root_path, 'static'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RepoConverter(BaseConverter):
|
||||||
|
|
||||||
|
"""Like the default :class:`UnicodeConverter`, but it allows matching
|
||||||
|
a single slash.
|
||||||
|
:param map: the :class:`Map`.
|
||||||
|
"""
|
||||||
|
regex = '[^/]*(/[^/]+)?'
|
||||||
|
#weight = 200
|
||||||
|
|
||||||
|
|
||||||
|
APP.url_map.converters['repo'] = RepoConverter
|
||||||
|
|
||||||
|
import pagure.doc_utils
|
||||||
|
import pagure.forms
|
||||||
|
import pagure.lib
|
||||||
|
import pagure.lib.git
|
||||||
|
import pagure.login_forms
|
||||||
|
import pagure.mail_logging
|
||||||
|
import pagure.proxy
|
||||||
|
|
||||||
|
# Only import flask_fas_openid if it is needed
|
||||||
|
if APP.config.get('PAGURE_AUTH', None) in ['fas', 'openid']:
|
||||||
|
from flask_fas_openid import FAS
|
||||||
|
FAS = FAS(APP)
|
||||||
|
|
||||||
|
@FAS.postlogin
|
||||||
|
def set_user(return_url):
|
||||||
|
''' After login method. '''
|
||||||
|
try:
|
||||||
|
pagure.lib.set_up_user(
|
||||||
|
session=SESSION,
|
||||||
|
username=flask.g.fas_user.username,
|
||||||
|
fullname=flask.g.fas_user.fullname,
|
||||||
|
default_email=flask.g.fas_user.email,
|
||||||
|
ssh_key=flask.g.fas_user.get('ssh_key'),
|
||||||
|
keydir=APP.config.get('GITOLITE_KEYDIR', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# If groups are managed outside pagure, set up the user at login
|
||||||
|
if not APP.config.get('ENABLE_GROUP_MNGT', False):
|
||||||
|
user = pagure.lib.search_user(
|
||||||
|
SESSION, username=flask.g.fas_user.username)
|
||||||
|
groups = set(user.groups)
|
||||||
|
fas_groups = set(flask.g.fas_user.groups)
|
||||||
|
# Add the new groups
|
||||||
|
for group in fas_groups - groups:
|
||||||
|
group = pagure.lib.search_groups(
|
||||||
|
SESSION, group_name=group)
|
||||||
|
if not group:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pagure.lib.add_user_to_group(
|
||||||
|
session=SESSION,
|
||||||
|
username=flask.g.fas_user.username,
|
||||||
|
group=group,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
is_admin=is_admin(),
|
||||||
|
)
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
LOG.debug(err)
|
||||||
|
# Remove the old groups
|
||||||
|
for group in groups - fas_groups:
|
||||||
|
try:
|
||||||
|
pagure.lib.delete_user_of_group(
|
||||||
|
session=SESSION,
|
||||||
|
username=flask.g.fas_user.username,
|
||||||
|
groupname=group,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
is_admin=is_admin(),
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
LOG.debug(err)
|
||||||
|
|
||||||
|
SESSION.commit()
|
||||||
|
except SQLAlchemyError as err:
|
||||||
|
SESSION.rollback()
|
||||||
|
LOG.debug(err)
|
||||||
|
LOG.exception(err)
|
||||||
|
flask.flash(
|
||||||
|
'Could not set up you as a user properly, please contact '
|
||||||
|
'an admin', 'error')
|
||||||
|
return flask.redirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
|
SESSION = pagure.lib.create_session(APP.config['DB_URL'])
|
||||||
|
REDIS = None
|
||||||
|
if APP.config['EVENTSOURCE_SOURCE'] or APP.config['WEBHOOK']:
|
||||||
|
pagure.lib.set_redis(
|
||||||
|
host=APP.config['REDIS_HOST'],
|
||||||
|
port=APP.config['REDIS_PORT'],
|
||||||
|
db=APP.config['REDIS_DB']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not APP.debug:
|
||||||
|
APP.logger.addHandler(pagure.mail_logging.get_mail_handler(
|
||||||
|
smtp_server=APP.config.get('SMTP_SERVER', '127.0.0.1'),
|
||||||
|
mail_admin=APP.config.get('MAIL_ADMIN', APP.config['EMAIL_ERROR'])
|
||||||
|
))
|
||||||
|
|
||||||
|
# Send classic logs into syslog
|
||||||
|
SHANDLER = logging.StreamHandler()
|
||||||
|
SHANDLER.setLevel(APP.config.get('LOG_LEVEL', 'INFO'))
|
||||||
|
APP.logger.addHandler(SHANDLER)
|
||||||
|
|
||||||
|
LOG = APP.logger
|
||||||
|
LOG.setLevel(APP.config.get('LOG_LEVEL', 'INFO'))
|
||||||
|
|
||||||
|
APP.wsgi_app = pagure.proxy.ReverseProxied(APP.wsgi_app)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated():
|
||||||
|
''' Utility function checking if the current user is logged in or not.
|
||||||
|
'''
|
||||||
|
return hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None
|
||||||
|
|
||||||
|
|
||||||
|
def logout():
|
||||||
|
auth = APP.config.get('PAGURE_AUTH', None)
|
||||||
|
if auth in ['fas', 'openid']:
|
||||||
|
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
|
||||||
|
FAS.logout()
|
||||||
|
elif auth == 'local':
|
||||||
|
import pagure.ui.login as login
|
||||||
|
login.logout()
|
||||||
|
|
||||||
|
|
||||||
|
def api_authenticated():
|
||||||
|
''' Utility function checking if the current user is logged in or not
|
||||||
|
in the API.
|
||||||
|
'''
|
||||||
|
return hasattr(flask.g, 'fas_user') \
|
||||||
|
and flask.g.fas_user is not None \
|
||||||
|
and hasattr(flask.g, 'token') \
|
||||||
|
and flask.g.token is not None
|
||||||
|
|
||||||
|
|
||||||
|
def admin_session_timedout():
|
||||||
|
''' Check if the current user has been authenticated for more than what
|
||||||
|
is allowed (defaults to 15 minutes).
|
||||||
|
If it is the case, the user is logged out and the method returns True,
|
||||||
|
otherwise it returns False.
|
||||||
|
'''
|
||||||
|
timedout = False
|
||||||
|
if not authenticated():
|
||||||
|
return True
|
||||||
|
login_time = flask.g.fas_user.login_time
|
||||||
|
# This is because flask_fas_openid will store this as a posix timestamp
|
||||||
|
if not isinstance(login_time, datetime.datetime):
|
||||||
|
login_time = datetime.datetime.utcfromtimestamp(login_time)
|
||||||
|
if (datetime.datetime.utcnow() - login_time) > \
|
||||||
|
APP.config.get('ADMIN_SESSION_LIFETIME',
|
||||||
|
datetime.timedelta(minutes=15)):
|
||||||
|
timedout = True
|
||||||
|
logout()
|
||||||
|
return timedout
|
||||||
|
|
||||||
|
|
||||||
|
def is_safe_url(target): # pragma: no cover
|
||||||
|
""" Checks that the target url is safe and sending to the current
|
||||||
|
website not some other malicious one.
|
||||||
|
"""
|
||||||
|
ref_url = urlparse.urlparse(flask.request.host_url)
|
||||||
|
test_url = urlparse.urlparse(
|
||||||
|
urlparse.urljoin(flask.request.host_url, target))
|
||||||
|
return test_url.scheme in ('http', 'https') and \
|
||||||
|
ref_url.netloc == test_url.netloc
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin():
|
||||||
|
""" Return whether the user is admin for this application or not. """
|
||||||
|
if not authenticated():
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = flask.g.fas_user
|
||||||
|
|
||||||
|
auth_method = APP.config.get('PAGURE_AUTH', None)
|
||||||
|
if auth_method == 'fas':
|
||||||
|
if not user.cla_done or len(user.groups) < 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
admin_users = APP.config.get('PAGURE_ADMIN_USERS', [])
|
||||||
|
if not isinstance(admin_users, list):
|
||||||
|
admin_users = [admin_users]
|
||||||
|
if user.username in admin_users:
|
||||||
|
return True
|
||||||
|
|
||||||
|
admins = APP.config['ADMIN_GROUP']
|
||||||
|
if isinstance(admins, basestring):
|
||||||
|
admins = [admins]
|
||||||
|
admins = set(admins)
|
||||||
|
groups = set(flask.g.fas_user.groups)
|
||||||
|
|
||||||
|
return not groups.isdisjoint(admins)
|
||||||
|
|
||||||
|
|
||||||
|
def is_repo_admin(repo_obj):
|
||||||
|
""" Return whether the user is an admin of the provided repo. """
|
||||||
|
if not authenticated():
|
||||||
|
return False
|
||||||
|
|
||||||
|
user = flask.g.fas_user.username
|
||||||
|
|
||||||
|
admin_users = APP.config.get('PAGURE_ADMIN_USERS', [])
|
||||||
|
if not isinstance(admin_users, list):
|
||||||
|
admin_users = [admin_users]
|
||||||
|
if user in admin_users:
|
||||||
|
return True
|
||||||
|
|
||||||
|
usergrps = [
|
||||||
|
usr.user
|
||||||
|
for grp in repo_obj.groups
|
||||||
|
for usr in grp.users]
|
||||||
|
|
||||||
|
return user == repo_obj.user.user or (
|
||||||
|
user in [usr.user for usr in repo_obj.users]
|
||||||
|
) or (user in usergrps)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_user_key_files():
|
||||||
|
""" Regenerate the key files used by gitolite.
|
||||||
|
"""
|
||||||
|
gitolite_home = APP.config.get('GITOLITE_HOME', None)
|
||||||
|
if gitolite_home:
|
||||||
|
users = pagure.lib.search_user(SESSION)
|
||||||
|
for user in users:
|
||||||
|
pagure.lib.update_user_ssh(SESSION, user, user.public_ssh_key,
|
||||||
|
APP.config.get('GITOLITE_KEYDIR', None))
|
||||||
|
pagure.lib.git.generate_gitolite_acls()
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(function):
|
||||||
|
""" Flask decorator to retrict access to logged in user.
|
||||||
|
If the auth system is ``fas`` it will also require that the user sign
|
||||||
|
the FPCA.
|
||||||
|
"""
|
||||||
|
auth_method = APP.config.get('PAGURE_AUTH', None)
|
||||||
|
|
||||||
|
@wraps(function)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
""" Decorated function, actually does the work. """
|
||||||
|
if flask.session.get('_justloggedout', False):
|
||||||
|
return flask.redirect(flask.url_for('.index'))
|
||||||
|
elif not authenticated():
|
||||||
|
return flask.redirect(
|
||||||
|
flask.url_for('auth_login', next=flask.request.url))
|
||||||
|
elif auth_method == 'fas' and not flask.g.fas_user.cla_done:
|
||||||
|
flask.flash('You must sign the FPCA (Fedora Project Contributor '
|
||||||
|
'Agreement) to use pagure', 'errors')
|
||||||
|
return flask.redirect(flask.url_for('.index'))
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
@APP.context_processor
|
||||||
|
def inject_variables():
|
||||||
|
""" With this decorator we can set some variables to all templates.
|
||||||
|
"""
|
||||||
|
user_admin = is_admin()
|
||||||
|
|
||||||
|
forkbuttonform = None
|
||||||
|
if authenticated():
|
||||||
|
forkbuttonform = pagure.forms.ConfirmationForm()
|
||||||
|
|
||||||
|
justlogedout = flask.session.get('_justloggedout', False)
|
||||||
|
if justlogedout:
|
||||||
|
flask.session['_justloggedout'] = None
|
||||||
|
|
||||||
|
def is_watching(reponame, username=None):
|
||||||
|
watch = False
|
||||||
|
if authenticated():
|
||||||
|
watch = pagure.lib.is_watching(
|
||||||
|
SESSION, flask.g.fas_user, reponame, repouser=username)
|
||||||
|
return watch
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
version=__version__,
|
||||||
|
admin=user_admin,
|
||||||
|
authenticated=authenticated(),
|
||||||
|
forkbuttonform=forkbuttonform,
|
||||||
|
is_watching=is_watching,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
@APP.before_request
|
||||||
|
def set_session():
|
||||||
|
""" Set the flask session as permanent. """
|
||||||
|
flask.session.permanent = True
|
||||||
|
|
||||||
|
|
||||||
|
@APP.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
"""404 Not Found page"""
|
||||||
|
return flask.render_template('not_found.html', error=error), 404
|
||||||
|
|
||||||
|
|
||||||
|
@APP.errorhandler(500)
|
||||||
|
def fatal_error(error): # pragma: no cover
|
||||||
|
"""500 Fatal Error page"""
|
||||||
|
return flask.render_template('fatal_error.html', error=error), 500
|
||||||
|
|
||||||
|
|
||||||
|
@APP.errorhandler(401)
|
||||||
|
def unauthorized(error): # pragma: no cover
|
||||||
|
"""401 Unauthorized page"""
|
||||||
|
return flask.render_template('unauthorized.html', error=error), 401
|
||||||
|
|
||||||
|
|
||||||
|
@APP.route('/login/', methods=('GET', 'POST'))
|
||||||
|
def auth_login(): # pragma: no cover
|
||||||
|
""" Method to log into the application using FAS OpenID. """
|
||||||
|
return_point = flask.url_for('index')
|
||||||
|
if 'next' in flask.request.args:
|
||||||
|
if is_safe_url(flask.request.args['next']):
|
||||||
|
return_point = flask.request.args['next']
|
||||||
|
|
||||||
|
if authenticated():
|
||||||
|
return flask.redirect(return_point)
|
||||||
|
|
||||||
|
admins = APP.config['ADMIN_GROUP']
|
||||||
|
if isinstance(admins, list):
|
||||||
|
admins = set(admins)
|
||||||
|
else: # pragma: no cover
|
||||||
|
admins = set([admins])
|
||||||
|
|
||||||
|
if APP.config.get('PAGURE_AUTH', None) in ['fas', 'openid']:
|
||||||
|
groups = set()
|
||||||
|
if not APP.config.get('ENABLE_GROUP_MNGT', False):
|
||||||
|
groups = [
|
||||||
|
group.group_name
|
||||||
|
for group in pagure.lib.search_groups(SESSION, group_type='user')
|
||||||
|
]
|
||||||
|
groups = set(groups).union(admins)
|
||||||
|
return FAS.login(return_url=return_point, groups=groups)
|
||||||
|
elif APP.config.get('PAGURE_AUTH', None) == 'local':
|
||||||
|
form = pagure.login_forms.LoginForm()
|
||||||
|
return flask.render_template(
|
||||||
|
'login/login.html',
|
||||||
|
next_url=return_point,
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@APP.route('/logout/')
|
||||||
|
def auth_logout(): # pragma: no cover
|
||||||
|
""" Method to log out from the application. """
|
||||||
|
return_point = flask.url_for('index')
|
||||||
|
if 'next' in flask.request.args:
|
||||||
|
if is_safe_url(flask.request.args['next']):
|
||||||
|
return_point = flask.request.args['next']
|
||||||
|
|
||||||
|
if not authenticated():
|
||||||
|
return flask.redirect(return_point)
|
||||||
|
|
||||||
|
logout()
|
||||||
|
flask.flash("You have been logged out")
|
||||||
|
flask.session['_justloggedout'] = True
|
||||||
|
return flask.redirect(return_point)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_file_in_tree(repo_obj, tree, filepath, bail_on_tree=False):
|
||||||
|
''' Retrieve the entry corresponding to the provided filename in a
|
||||||
|
given tree.
|
||||||
|
'''
|
||||||
|
|
||||||
|
filename = filepath[0]
|
||||||
|
if isinstance(tree, pygit2.Blob):
|
||||||
|
return
|
||||||
|
for entry in tree:
|
||||||
|
fname = entry.name.decode('utf-8')
|
||||||
|
if fname == filename:
|
||||||
|
if len(filepath) == 1:
|
||||||
|
blob = repo_obj.get(entry.id)
|
||||||
|
# If we can't get the content (for example: an empty folder)
|
||||||
|
if blob is None:
|
||||||
|
return
|
||||||
|
# If we get a tree instead of a blob, let's escape
|
||||||
|
if isinstance(blob, pygit2.Tree) and bail_on_tree:
|
||||||
|
return blob
|
||||||
|
content = blob.data
|
||||||
|
# If it's a (sane) symlink, we try a single-level dereference
|
||||||
|
if entry.filemode == pygit2.GIT_FILEMODE_LINK \
|
||||||
|
and os.path.normpath(content) == content \
|
||||||
|
and not os.path.isabs(content):
|
||||||
|
try:
|
||||||
|
dereferenced = tree[content]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if dereferenced.filemode == pygit2.GIT_FILEMODE_BLOB:
|
||||||
|
blob = repo_obj[dereferenced.oid]
|
||||||
|
|
||||||
|
return blob
|
||||||
|
else:
|
||||||
|
nextitem = repo_obj[entry.oid]
|
||||||
|
# If we can't get the content (for example: an empty folder)
|
||||||
|
if nextitem is None:
|
||||||
|
return
|
||||||
|
return __get_file_in_tree(
|
||||||
|
repo_obj, nextitem, filepath[1:],
|
||||||
|
bail_on_tree=bail_on_tree)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repo_path(repo):
|
||||||
|
""" Return the path of the git repository corresponding to the provided
|
||||||
|
Repository object from the DB.
|
||||||
|
"""
|
||||||
|
repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path)
|
||||||
|
|
||||||
|
if not os.path.exists(repopath):
|
||||||
|
flask.abort(404, 'No git repo found')
|
||||||
|
|
||||||
|
return repopath
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_repo_path(remote_git, branch_from, loop=False):
|
||||||
|
""" Return the path of the remote git repository corresponding to the
|
||||||
|
provided information.
|
||||||
|
"""
|
||||||
|
repopath = os.path.join(
|
||||||
|
APP.config['REMOTE_GIT_FOLDER'],
|
||||||
|
werkzeug.secure_filename('%s_%s' % (remote_git, branch_from))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(repopath):
|
||||||
|
try:
|
||||||
|
pygit2.clone_repository(
|
||||||
|
remote_git, repopath, checkout_branch=branch_from)
|
||||||
|
except Exception as err:
|
||||||
|
LOG.debug(err)
|
||||||
|
LOG.exception(err)
|
||||||
|
flask.abort(500, 'Could not clone the remote git repository')
|
||||||
|
else:
|
||||||
|
repo = pagure.lib.repo.PagureRepo(repopath)
|
||||||
|
try:
|
||||||
|
repo.pull(branch=branch_from, force=True)
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
LOG.debug(err)
|
||||||
|
LOG.exception(err)
|
||||||
|
flask.abort(500, err.message)
|
||||||
|
|
||||||
|
return repopath
|
||||||
|
|
||||||
|
|
||||||
|
# Import the application
|
||||||
|
import pagure.ui.app
|
||||||
|
import pagure.ui.admin
|
||||||
|
import pagure.ui.fork
|
||||||
|
import pagure.ui.groups
|
||||||
|
if APP.config.get('ENABLE_TICKETS', True):
|
||||||
|
import pagure.ui.issues
|
||||||
|
import pagure.ui.plugins
|
||||||
|
import pagure.ui.repo
|
||||||
|
|
||||||
|
from pagure.api import API
|
||||||
|
APP.register_blueprint(API)
|
||||||
|
|
||||||
|
import pagure.internal
|
||||||
|
APP.register_blueprint(pagure.internal.PV)
|
||||||
|
|
||||||
|
|
||||||
|
# Only import the login controller if the app is set up for local login
|
||||||
|
if APP.config.get('PAGURE_AUTH', None) == 'local':
|
||||||
|
import pagure.ui.login as login
|
||||||
|
APP.before_request(login._check_session_cookie)
|
||||||
|
APP.after_request(login._send_session_cookie)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
@APP.teardown_request
|
||||||
|
def shutdown_session(exception=None):
|
||||||
|
""" Remove the DB session at the end of each request. """
|
||||||
|
SESSION.remove()
|
496
pagure/api/__init__.py
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
API namespace version 0.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
|
||||||
|
import docutils
|
||||||
|
import enum
|
||||||
|
import flask
|
||||||
|
import markupsafe
|
||||||
|
|
||||||
|
API = flask.Blueprint('api_ns', __name__, url_prefix='/api/0')
|
||||||
|
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.lib
|
||||||
|
from pagure import __api_version__, APP, SESSION, authenticated
|
||||||
|
from pagure.doc_utils import load_doc, modify_rst, modify_html
|
||||||
|
from pagure.exceptions import APIError
|
||||||
|
|
||||||
|
|
||||||
|
def preload_docs(endpoint):
|
||||||
|
''' Utility to load an RST file and turn it into fancy HTML. '''
|
||||||
|
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
fname = os.path.join(here, '..', 'doc', endpoint + '.rst')
|
||||||
|
with codecs.open(fname, 'r', 'utf-8') as f:
|
||||||
|
rst = f.read()
|
||||||
|
|
||||||
|
rst = modify_rst(rst)
|
||||||
|
api_docs = docutils.examples.html_body(rst)
|
||||||
|
api_docs = modify_html(api_docs)
|
||||||
|
api_docs = markupsafe.Markup(api_docs)
|
||||||
|
return api_docs
|
||||||
|
|
||||||
|
|
||||||
|
APIDOC = preload_docs('api')
|
||||||
|
|
||||||
|
|
||||||
|
class APIERROR(enum.Enum):
|
||||||
|
""" Clast listing as Enum all the possible error thrown by the API.
|
||||||
|
"""
|
||||||
|
ENOCODE = 'Variable message describing the issue'
|
||||||
|
ENOPROJECT = 'Project not found'
|
||||||
|
ENOPROJECTS = 'No projects found'
|
||||||
|
ETRACKERDISABLED = 'Issue tracker disabled for this project'
|
||||||
|
EDBERROR = 'An error occured at the database level and prevent the ' \
|
||||||
|
'action from reaching completion'
|
||||||
|
EINVALIDREQ = 'Invalid or incomplete input submited'
|
||||||
|
EINVALIDTOK = 'Invalid or expired token. Please visit %s to get or '\
|
||||||
|
'renew your API token.' % APP.config['APP_URL']
|
||||||
|
ENOISSUE = 'Issue not found'
|
||||||
|
EISSUENOTALLOWED = 'You are not allowed to view this issue'
|
||||||
|
EPULLREQUESTSDISABLED = 'Pull-Request have been deactivated for this '\
|
||||||
|
'project'
|
||||||
|
ENOREQ = 'Pull-Request not found'
|
||||||
|
ENOPRCLOSE = 'You are not allowed to merge/close pull-request for '\
|
||||||
|
'this project'
|
||||||
|
EPRSCORE = 'This request does not have the minimum review score '\
|
||||||
|
'necessary to be merged'
|
||||||
|
ENOTASSIGNEE = 'Only the assignee can merge this review'
|
||||||
|
ENOTASSIGNED = 'This request must be assigned to be merged'
|
||||||
|
ENOUSER = 'No such user found'
|
||||||
|
ENOCOMMENT = 'Comment not found'
|
||||||
|
ENEWPROJECTDISABLED = 'Creating project have been disabled for this '\
|
||||||
|
'instance'
|
||||||
|
|
||||||
|
|
||||||
|
def check_api_acls(acls, optional=False):
|
||||||
|
''' Checks if the user provided an API token with its request and if
|
||||||
|
this token allows the user to access the endpoint desired.
|
||||||
|
'''
|
||||||
|
flask.g.token = None
|
||||||
|
flask.g.user = None
|
||||||
|
token = None
|
||||||
|
token_str = None
|
||||||
|
|
||||||
|
if authenticated():
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'Authorization' in flask.request.headers:
|
||||||
|
authorization = flask.request.headers['Authorization']
|
||||||
|
if 'token' in authorization:
|
||||||
|
token_str = authorization.split('token', 1)[1].strip()
|
||||||
|
|
||||||
|
token_auth = False
|
||||||
|
if token_str:
|
||||||
|
token = pagure.lib.get_api_token(SESSION, token_str)
|
||||||
|
if token and not token.expired:
|
||||||
|
if acls and set(token.acls_list).intersection(set(acls)):
|
||||||
|
token_auth = True
|
||||||
|
flask.g.fas_user = token.user
|
||||||
|
flask.g.token = token
|
||||||
|
elif not acls and optional:
|
||||||
|
token_auth = True
|
||||||
|
flask.g.fas_user = token.user
|
||||||
|
flask.g.token = token
|
||||||
|
elif optional:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not token_auth:
|
||||||
|
output = {
|
||||||
|
'error_code': APIERROR.EINVALIDTOK.name,
|
||||||
|
'error': APIERROR.EINVALIDTOK.value,
|
||||||
|
}
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
jsonout.status_code = 401
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
def api_login_required(acls=None):
|
||||||
|
''' Decorator used to indicate that authentication is required for some
|
||||||
|
API endpoint.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def decorator(fn):
|
||||||
|
''' The decorator of the function '''
|
||||||
|
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
''' Actually does the job with the arguments provided. '''
|
||||||
|
|
||||||
|
response = check_api_acls(acls)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def api_login_optional(acls=None):
|
||||||
|
''' Decorator used to indicate that authentication is optional for some
|
||||||
|
API endpoint.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def decorator(fn):
|
||||||
|
''' The decorator of the function '''
|
||||||
|
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
''' Actually does the job with the arguments provided. '''
|
||||||
|
|
||||||
|
response = check_api_acls(acls, optional=True)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def api_method(function):
|
||||||
|
''' Runs an API endpoint and catch all the APIException thrown. '''
|
||||||
|
|
||||||
|
@functools.wraps(function)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
result = function(*args, **kwargs)
|
||||||
|
except APIError as e:
|
||||||
|
if e.error_code in [APIERROR.EDBERROR]:
|
||||||
|
APP.logger.exception(e)
|
||||||
|
|
||||||
|
if e.error_code in [APIERROR.ENOCODE]:
|
||||||
|
response = flask.jsonify(
|
||||||
|
{
|
||||||
|
'error': e.error,
|
||||||
|
'error_code': e.error_code.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = flask.jsonify(
|
||||||
|
{
|
||||||
|
'error': e.error_code.value,
|
||||||
|
'error_code': e.error_code.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.status_code = e.status_code
|
||||||
|
else:
|
||||||
|
response = result
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||||
|
from pagure.api import issue
|
||||||
|
from pagure.api import fork
|
||||||
|
from pagure.api import project
|
||||||
|
from pagure.api import user
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/version/')
|
||||||
|
@API.route('/version')
|
||||||
|
def api_version():
|
||||||
|
'''
|
||||||
|
API Version
|
||||||
|
-----------
|
||||||
|
Get the current API version.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/version
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"version": "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
return flask.jsonify({'version': __api_version__})
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/users/')
|
||||||
|
@API.route('/users')
|
||||||
|
def api_users():
|
||||||
|
'''
|
||||||
|
List users
|
||||||
|
-----------
|
||||||
|
Retrieve users that have logged into the Paugre instance.
|
||||||
|
This can then be used as input for autocompletion in some forms/fields.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/users
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+----------+---------------+------------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+==========+===============+==============================+
|
||||||
|
| ``pattern`` | string | Optional | | Filters the starting |
|
||||||
|
| | | | letters of the usernames |
|
||||||
|
+---------------+----------+---------------+------------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"total_users": 2,
|
||||||
|
"users": ["user1", "user2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
pattern = flask.request.args.get('pattern', None)
|
||||||
|
if pattern is not None and not pattern.endswith('*'):
|
||||||
|
pattern += '*'
|
||||||
|
|
||||||
|
users = pagure.lib.search_user(SESSION, pattern=pattern)
|
||||||
|
|
||||||
|
return flask.jsonify(
|
||||||
|
{
|
||||||
|
'total_users': len(users),
|
||||||
|
'users': [user.username for user in users],
|
||||||
|
'mention': [{
|
||||||
|
'username': user.username,
|
||||||
|
'name': user.fullname,
|
||||||
|
'image': pagure.lib.avatar_url_from_openid(user.default_email,
|
||||||
|
size=16)
|
||||||
|
} for user in users]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/tags')
|
||||||
|
@API.route('/<repo>/tags/')
|
||||||
|
@API.route('/fork/<username>/<repo>/tags')
|
||||||
|
@API.route('/fork/<username>/<repo>/tags/')
|
||||||
|
def api_project_tags(repo, username=None):
|
||||||
|
'''
|
||||||
|
List all the tags of a project
|
||||||
|
------------------------------
|
||||||
|
List the tags made on the project's issues.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/tags
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/tags
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+==========+===============+==========================+
|
||||||
|
| ``pattern`` | string | Optional | | Filters the starting |
|
||||||
|
| | | | letters of the tags |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"total_tags": 2,
|
||||||
|
"tags": ["tag1", "tag2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
pattern = flask.request.args.get('pattern', None)
|
||||||
|
if pattern is not None and not pattern.endswith('*'):
|
||||||
|
pattern += '*'
|
||||||
|
|
||||||
|
project_obj = pagure.lib.get_project(SESSION, repo, username)
|
||||||
|
if not project_obj:
|
||||||
|
output = {'output': 'notok', 'error': 'Project not found'}
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
jsonout.status_code = 404
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
tags = pagure.lib.get_tags_of_project(
|
||||||
|
SESSION, project_obj, pattern=pattern)
|
||||||
|
|
||||||
|
return flask.jsonify(
|
||||||
|
{
|
||||||
|
'total_tags': len(tags),
|
||||||
|
'tags': [tag.tag for tag in tags]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/groups/')
|
||||||
|
@API.route('/groups')
|
||||||
|
def api_groups():
|
||||||
|
'''
|
||||||
|
List groups
|
||||||
|
-----------
|
||||||
|
Retrieve groups on this Pagure instance.
|
||||||
|
This can then be used as input for autocompletion in some forms/fields.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/groups
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+==========+===============+==========================+
|
||||||
|
| ``pattern`` | string | Optional | | Filters the starting |
|
||||||
|
| | | | letters of the group |
|
||||||
|
| | | | names |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"total_groups": 2,
|
||||||
|
"groups": ["group1", "group2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
pattern = flask.request.args.get('pattern', None)
|
||||||
|
if pattern is not None and not pattern.endswith('*'):
|
||||||
|
pattern += '*'
|
||||||
|
|
||||||
|
groups = pagure.lib.search_groups(SESSION, pattern=pattern)
|
||||||
|
|
||||||
|
return flask.jsonify(
|
||||||
|
{
|
||||||
|
'total_groups': len(groups),
|
||||||
|
'groups': [group.group_name for group in groups]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/error_codes/')
|
||||||
|
@API.route('/error_codes')
|
||||||
|
def api_error_codes():
|
||||||
|
'''
|
||||||
|
Error codes
|
||||||
|
------------
|
||||||
|
Get a dictionary (hash) of all error codes.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/error_codes
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
ENOCODE: 'Variable message describing the issue',
|
||||||
|
ENOPROJECT: 'Project not found',
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
errors = {val.name: val.value for val in APIERROR.__members__.values()}
|
||||||
|
|
||||||
|
return flask.jsonify(errors)
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/')
|
||||||
|
def api():
|
||||||
|
''' Display the api information page. '''
|
||||||
|
api_git_tags_doc = load_doc(project.api_git_tags)
|
||||||
|
api_projects_doc = load_doc(project.api_projects)
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||||
|
issues.append(load_doc(issue.api_new_issue))
|
||||||
|
issues.append(load_doc(issue.api_view_issues))
|
||||||
|
issues.append(load_doc(issue.api_view_issue))
|
||||||
|
issues.append(load_doc(issue.api_view_issue_comment))
|
||||||
|
issues.append(load_doc(issue.api_comment_issue))
|
||||||
|
|
||||||
|
api_pull_request_views_doc = load_doc(fork.api_pull_request_views)
|
||||||
|
api_pull_request_view_doc = load_doc(fork.api_pull_request_view)
|
||||||
|
api_pull_request_merge_doc = load_doc(fork.api_pull_request_merge)
|
||||||
|
api_pull_request_close_doc = load_doc(fork.api_pull_request_close)
|
||||||
|
api_pull_request_add_comment_doc = load_doc(
|
||||||
|
fork.api_pull_request_add_comment)
|
||||||
|
api_pull_request_add_flag_doc = load_doc(fork.api_pull_request_add_flag)
|
||||||
|
|
||||||
|
api_new_project_doc = load_doc(project.api_new_project)
|
||||||
|
|
||||||
|
api_version_doc = load_doc(api_version)
|
||||||
|
api_users_doc = load_doc(api_users)
|
||||||
|
api_view_user_doc = load_doc(user.api_view_user)
|
||||||
|
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||||
|
api_project_tags_doc = load_doc(api_project_tags)
|
||||||
|
api_groups_doc = load_doc(api_groups)
|
||||||
|
api_error_codes_doc = load_doc(api_error_codes)
|
||||||
|
|
||||||
|
extras = [
|
||||||
|
api_version_doc,
|
||||||
|
api_error_codes_doc,
|
||||||
|
]
|
||||||
|
|
||||||
|
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||||
|
extras.append(api_project_tags_doc)
|
||||||
|
|
||||||
|
return flask.render_template(
|
||||||
|
'api.html',
|
||||||
|
version=__api_version__.split('.'),
|
||||||
|
api_doc=APIDOC,
|
||||||
|
projects=[
|
||||||
|
api_new_project_doc,
|
||||||
|
api_git_tags_doc,
|
||||||
|
api_projects_doc,
|
||||||
|
],
|
||||||
|
issues=issues,
|
||||||
|
requests=[
|
||||||
|
api_pull_request_views_doc,
|
||||||
|
api_pull_request_view_doc,
|
||||||
|
api_pull_request_merge_doc,
|
||||||
|
api_pull_request_close_doc,
|
||||||
|
api_pull_request_add_comment_doc,
|
||||||
|
api_pull_request_add_flag_doc,
|
||||||
|
],
|
||||||
|
users=[
|
||||||
|
api_users_doc,
|
||||||
|
api_view_user_doc,
|
||||||
|
api_groups_doc,
|
||||||
|
],
|
||||||
|
extras=extras,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@APP.route('/api/')
|
||||||
|
@APP.route('/api')
|
||||||
|
def api_redirect():
|
||||||
|
''' Redirects the user to the API documentation page.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return flask.redirect(flask.url_for('api_ns.api'))
|
650
pagure/api/fork.py
Normal file
|
@ -0,0 +1,650 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.exceptions
|
||||||
|
import pagure.lib
|
||||||
|
from pagure import APP, SESSION, is_repo_admin
|
||||||
|
from pagure.api import API, api_method, api_login_required, APIERROR
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-requests')
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-requests')
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_views(repo, username=None):
|
||||||
|
"""
|
||||||
|
List project's Pull-Requests
|
||||||
|
----------------------------
|
||||||
|
Retrieve pull requests of a project.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/pull-requests
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/pull-requests
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+----------+--------------+----------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+==========+==============+============================+
|
||||||
|
| ``status`` | string | Optional | | Filter the status of |
|
||||||
|
| | | | pull requests. Default: |
|
||||||
|
| | | | ``True`` (opened pull |
|
||||||
|
| | | | requests), can be ``0`` |
|
||||||
|
| | | | or ``closed`` for closed |
|
||||||
|
| | | | requests or ``Merged`` |
|
||||||
|
| | | | for merged requests. |
|
||||||
|
| | | | ``All`` returns closed, |
|
||||||
|
| | | | merged and open requests.|
|
||||||
|
+---------------+----------+--------------+----------------------------+
|
||||||
|
| ``assignee`` | string | Optional | | Filter the assignee of |
|
||||||
|
| | | | pull requests |
|
||||||
|
+---------------+----------+--------------+----------------------------+
|
||||||
|
| ``author`` | string | Optional | | Filter the author of |
|
||||||
|
| | | | pull requests |
|
||||||
|
+---------------+----------+--------------+----------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"assignee": null,
|
||||||
|
"author": null,
|
||||||
|
"status": true
|
||||||
|
},
|
||||||
|
"total_requests": 1,
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"assignee": null,
|
||||||
|
"branch": "master",
|
||||||
|
"branch_from": "master",
|
||||||
|
"closed_at": null,
|
||||||
|
"closed_by": null,
|
||||||
|
"comments": [],
|
||||||
|
"commit_start": null,
|
||||||
|
"commit_stop": null,
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"id": 1,
|
||||||
|
"project": {
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"description": "test project #1",
|
||||||
|
"id": 1,
|
||||||
|
"name": "test",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repo_from": {
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"description": "test project #1",
|
||||||
|
"id": 1,
|
||||||
|
"name": "test",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": true,
|
||||||
|
"title": "test pull-request",
|
||||||
|
"uid": "1431414800",
|
||||||
|
"updated_on": "1431414800",
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
status = flask.request.args.get('status', True)
|
||||||
|
assignee = flask.request.args.get('assignee', None)
|
||||||
|
author = flask.request.args.get('author', None)
|
||||||
|
|
||||||
|
requests = []
|
||||||
|
if str(status).lower() in ['0', 'false', 'closed']:
|
||||||
|
requests = pagure.lib.search_pull_requests(
|
||||||
|
SESSION,
|
||||||
|
project_id=repo.id,
|
||||||
|
status=False,
|
||||||
|
assignee=assignee,
|
||||||
|
author=author)
|
||||||
|
|
||||||
|
elif str(status).lower() == 'all':
|
||||||
|
requests = pagure.lib.search_pull_requests(
|
||||||
|
SESSION,
|
||||||
|
project_id=repo.id,
|
||||||
|
status=None,
|
||||||
|
assignee=assignee,
|
||||||
|
author=author)
|
||||||
|
|
||||||
|
else:
|
||||||
|
requests = pagure.lib.search_pull_requests(
|
||||||
|
SESSION,
|
||||||
|
project_id=repo.id,
|
||||||
|
assignee=assignee,
|
||||||
|
author=author,
|
||||||
|
status=status)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify({
|
||||||
|
'total_requests': len(requests),
|
||||||
|
'requests': [
|
||||||
|
request.to_json(public=True, api=True)
|
||||||
|
for request in requests],
|
||||||
|
'args': {
|
||||||
|
'status': status,
|
||||||
|
'assignee': assignee,
|
||||||
|
'author': author,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-request/<int:requestid>')
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>')
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_view(repo, requestid, username=None):
|
||||||
|
"""
|
||||||
|
Pull-request information
|
||||||
|
------------------------
|
||||||
|
Retrieve information of a specific pull request.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/pull-request/<request id>
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/pull-request/<request id>
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"assignee": null,
|
||||||
|
"branch": "master",
|
||||||
|
"branch_from": "master",
|
||||||
|
"closed_at": null,
|
||||||
|
"closed_by": null,
|
||||||
|
"comments": [],
|
||||||
|
"commit_start": null,
|
||||||
|
"commit_stop": null,
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"id": 1,
|
||||||
|
"project": {
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"description": "test project #1",
|
||||||
|
"id": 1,
|
||||||
|
"name": "test",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repo_from": {
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"description": "test project #1",
|
||||||
|
"id": 1,
|
||||||
|
"name": "test",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": true,
|
||||||
|
"title": "test pull-request",
|
||||||
|
"uid": "1431414800",
|
||||||
|
"updated_on": "1431414800",
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
request = pagure.lib.search_pull_requests(
|
||||||
|
SESSION, project_id=repo.id, requestid=requestid)
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(request.to_json(public=True, api=True))
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-request/<int:requestid>/merge', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/merge',
|
||||||
|
methods=['POST'])
|
||||||
|
@api_login_required(acls=['pull_request_merge'])
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_merge(repo, requestid, username=None):
|
||||||
|
"""
|
||||||
|
Merge a pull-request
|
||||||
|
--------------------
|
||||||
|
Instruct Paugre to merge a pull request.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/pull-request/<request id>/merge
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/merge
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Changes merged!"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
request = pagure.lib.search_pull_requests(
|
||||||
|
SESSION, project_id=repo.id, requestid=requestid)
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||||
|
|
||||||
|
if not is_repo_admin(repo):
|
||||||
|
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
|
||||||
|
|
||||||
|
if repo.settings.get('Only_assignee_can_merge_pull-request', False):
|
||||||
|
if not request.assignee:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.ENOTASSIGNED)
|
||||||
|
|
||||||
|
if request.assignee.username != flask.g.fas_user.username:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.ENOTASSIGNEE)
|
||||||
|
|
||||||
|
threshold = repo.settings.get('Minimum_score_to_merge_pull-request', -1)
|
||||||
|
if threshold > 0 and int(request.score) < int(threshold):
|
||||||
|
raise pagure.exceptions.APIError(403, error_code=APIERROR.EPRSCORE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = pagure.lib.git.merge_pull_request(
|
||||||
|
SESSION, request, flask.g.fas_user.username,
|
||||||
|
APP.config['REQUESTS_FOLDER'])
|
||||||
|
output['message'] = message
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-request/<int:requestid>/close', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/close',
|
||||||
|
methods=['POST'])
|
||||||
|
@api_login_required(acls=['pull_request_close'])
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_close(repo, requestid, username=None):
|
||||||
|
"""
|
||||||
|
Close a pull-request
|
||||||
|
--------------------
|
||||||
|
Instruct Pagure to close a pull request.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/pull-request/<request id>/close
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/close
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Pull-request closed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
request = pagure.lib.search_pull_requests(
|
||||||
|
SESSION, project_id=repo.id, requestid=requestid)
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||||
|
|
||||||
|
if not is_repo_admin(repo):
|
||||||
|
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pagure.lib.close_pull_request(
|
||||||
|
SESSION, request, flask.g.fas_user.username,
|
||||||
|
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||||
|
merged=False)
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = 'Pull-request closed!'
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
SESSION.rollback()
|
||||||
|
APP.logger.exception(err)
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-request/<int:requestid>/comment',
|
||||||
|
methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/comment',
|
||||||
|
methods=['POST'])
|
||||||
|
@api_login_required(acls=['pull_request_comment'])
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_add_comment(repo, requestid, username=None):
|
||||||
|
"""
|
||||||
|
Comment on a pull-request
|
||||||
|
-------------------------
|
||||||
|
Add comment to a pull request.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/pull-request/<request id>/comment
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/comment
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+=========+==============+=============================+
|
||||||
|
| ``comment`` | string | Mandatory | | The comment to add |
|
||||||
|
| | | | to the pull request |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``commit`` | string | Optional | | The hash of the specific |
|
||||||
|
| | | | commit you wish to |
|
||||||
|
| | | | comment on |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``filename`` | string | Optional | | The filename of the |
|
||||||
|
| | | | specific file you wish |
|
||||||
|
| | | | to comment on |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``row`` | int | Optional | | Used in combination |
|
||||||
|
| | | | with filename to comment |
|
||||||
|
| | | | on a specific row |
|
||||||
|
| | | | of a file |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``tree_id`` | string | Optional | | The identifier of the |
|
||||||
|
| | | | git tree as it was when |
|
||||||
|
| | | | the comment was added |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Comment added"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
if repo.fullname != flask.g.token.project.fullname:
|
||||||
|
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
request = pagure.lib.search_pull_requests(
|
||||||
|
SESSION, project_id=repo.id, requestid=requestid)
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||||
|
|
||||||
|
form = pagure.forms.AddPullRequestCommentForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
comment = form.comment.data
|
||||||
|
commit = form.commit.data or None
|
||||||
|
filename = form.filename.data or None
|
||||||
|
tree_id = form.tree_id.data or None
|
||||||
|
row = form.row.data or None
|
||||||
|
try:
|
||||||
|
# New comment
|
||||||
|
message = pagure.lib.add_pull_request_comment(
|
||||||
|
SESSION,
|
||||||
|
request=request,
|
||||||
|
commit=commit,
|
||||||
|
tree_id=tree_id,
|
||||||
|
filename=filename,
|
||||||
|
row=row,
|
||||||
|
comment=comment,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = message
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
APP.logger.exception(err)
|
||||||
|
SESSION.rollback()
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/pull-request/<int:requestid>/flag',
|
||||||
|
methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/flag',
|
||||||
|
methods=['POST'])
|
||||||
|
@api_login_required(acls=['pull_request_flag'])
|
||||||
|
@api_method
|
||||||
|
def api_pull_request_add_flag(repo, requestid, username=None):
|
||||||
|
"""
|
||||||
|
Flag a pull-request
|
||||||
|
-------------------
|
||||||
|
Add or edit flags on a pull-request.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/pull-request/<request id>/flag
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/flag
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+=========+==============+=============================+
|
||||||
|
| ``username`` | string | Mandatory | | The name of the |
|
||||||
|
| | | | application to be |
|
||||||
|
| | | | presented to users |
|
||||||
|
| | | | on the pull request page |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``percent`` | int | Mandatory | | A percentage of |
|
||||||
|
| | | | completion compared to |
|
||||||
|
| | | | the goal. The percentage |
|
||||||
|
| | | | also determine the |
|
||||||
|
| | | | background color of the |
|
||||||
|
| | | | flag on the pull-request |
|
||||||
|
| | | | page |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``comment`` | string | Mandatory | | A short message |
|
||||||
|
| | | | summarizing the |
|
||||||
|
| | | | presented results |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``url`` | string | Mandatory | | A URL to the result |
|
||||||
|
| | | | of this flag |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``uid`` | string | Optional | | A unique identifier used |
|
||||||
|
| | | | to identify a flag on a |
|
||||||
|
| | | | pull-request. If the |
|
||||||
|
| | | | provided UID matches an |
|
||||||
|
| | | | existing one, then the |
|
||||||
|
| | | | API call will update the |
|
||||||
|
| | | | existing one rather than |
|
||||||
|
| | | | create a new one. |
|
||||||
|
| | | | Maximum Length: 32 |
|
||||||
|
| | | | characters. Default: an |
|
||||||
|
| | | | auto generated UID |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
| ``commit`` | string | Optional | | The hash of the commit |
|
||||||
|
| | | | you use |
|
||||||
|
+---------------+---------+--------------+-----------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Flag added"
|
||||||
|
}
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Flag updated"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('pull_requests', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||||
|
|
||||||
|
if repo.fullname != flask.g.token.project.fullname:
|
||||||
|
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
request = pagure.lib.search_pull_requests(
|
||||||
|
SESSION, project_id=repo.id, requestid=requestid)
|
||||||
|
|
||||||
|
if not request:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||||
|
|
||||||
|
form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
username = form.username.data
|
||||||
|
percent = form.percent.data
|
||||||
|
comment = form.comment.data.strip()
|
||||||
|
url = form.url.data.strip()
|
||||||
|
uid = form.uid.data.strip() if form.uid.data else None
|
||||||
|
try:
|
||||||
|
# New Flag
|
||||||
|
message = pagure.lib.add_pull_request_flag(
|
||||||
|
SESSION,
|
||||||
|
request=request,
|
||||||
|
username=username,
|
||||||
|
percent=percent,
|
||||||
|
comment=comment,
|
||||||
|
url=url,
|
||||||
|
uid=uid,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = message
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
APP.logger.exception(err)
|
||||||
|
SESSION.rollback()
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
729
pagure/api/issue.py
Normal file
|
@ -0,0 +1,729 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.exceptions
|
||||||
|
import pagure.lib
|
||||||
|
from pagure import APP, SESSION, is_repo_admin, api_authenticated
|
||||||
|
from pagure.api import (
|
||||||
|
API, api_method, api_login_required, api_login_optional, APIERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/new_issue', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/new_issue', methods=['POST'])
|
||||||
|
@api_login_required(acls=['issue_create'])
|
||||||
|
@api_method
|
||||||
|
def api_new_issue(repo, username=None):
|
||||||
|
"""
|
||||||
|
Create a new issue
|
||||||
|
------------------
|
||||||
|
Open a new issue on a project.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/new_issue
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/new_issue
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+--------------+----------+--------------+-----------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+==============+==========+==============+=============================+
|
||||||
|
| ``title`` | string | Mandatory | The title of the issue |
|
||||||
|
+--------------+----------+--------------+-----------------------------+
|
||||||
|
| ``content`` | string | Mandatory | | The description of the |
|
||||||
|
| | | | issue |
|
||||||
|
+--------------+----------+--------------+-----------------------------+
|
||||||
|
| ``private`` | boolean | Optional | | Include this key if |
|
||||||
|
| | | | you want a private issue |
|
||||||
|
| | | | to be created |
|
||||||
|
+--------------+----------+--------------+-----------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Issue created"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
form = pagure.forms.IssueFormSimplied(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
title = form.title.data
|
||||||
|
content = form.issue_content.data
|
||||||
|
private = str(form.private.data).lower() in ['true', '1']
|
||||||
|
|
||||||
|
try:
|
||||||
|
issue = pagure.lib.new_issue(
|
||||||
|
SESSION,
|
||||||
|
repo=repo,
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
private=private,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.flush()
|
||||||
|
# If there is a file attached, attach it.
|
||||||
|
filestream = flask.request.files.get('filestream')
|
||||||
|
if filestream and '<!!image>' in issue.content:
|
||||||
|
new_filename = pagure.lib.git.add_file_to_git(
|
||||||
|
repo=repo,
|
||||||
|
issue=issue,
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
user=flask.g.fas_user,
|
||||||
|
filename=filestream.filename,
|
||||||
|
filestream=filestream.stream,
|
||||||
|
)
|
||||||
|
# Replace the <!!image> tag in the comment with the link
|
||||||
|
# to the actual image
|
||||||
|
filelocation = flask.url_for(
|
||||||
|
'view_issue_raw_file',
|
||||||
|
repo=repo.name,
|
||||||
|
username=username,
|
||||||
|
filename=new_filename,
|
||||||
|
)
|
||||||
|
new_filename = new_filename.split('-', 1)[1]
|
||||||
|
url = '[![%s](%s)](%s)' % (
|
||||||
|
new_filename, filelocation, filelocation)
|
||||||
|
issue.content = issue.content.replace('<!!image>', url)
|
||||||
|
SESSION.add(issue)
|
||||||
|
SESSION.flush()
|
||||||
|
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = 'Issue created'
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
SESSION.rollback()
|
||||||
|
APP.logger.exception(err)
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issues')
|
||||||
|
@API.route('/fork/<username>/<repo>/issues')
|
||||||
|
@api_login_optional()
|
||||||
|
@api_method
|
||||||
|
def api_view_issues(repo, username=None):
|
||||||
|
"""
|
||||||
|
List project's issues
|
||||||
|
---------------------
|
||||||
|
List issues of a project.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/issues
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/issues
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+---------+--------------+---------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+=========+==============+===========================+
|
||||||
|
| ``status`` | string | Optional | | Filters the status of |
|
||||||
|
| | | | issues. Fetches all the |
|
||||||
|
| | | | issues if status is |
|
||||||
|
| | | | ``all``. Default: |
|
||||||
|
| | | | ``Open`` |
|
||||||
|
+---------------+---------+--------------+---------------------------+
|
||||||
|
| ``tags`` | string | Optional | | A list of tags you |
|
||||||
|
| | | | wish to filter. If |
|
||||||
|
| | | | you want to filter |
|
||||||
|
| | | | for issues not having |
|
||||||
|
| | | | a tag, add an |
|
||||||
|
| | | | exclamation mark in |
|
||||||
|
| | | | front of it |
|
||||||
|
+---------------+---------+--------------+---------------------------+
|
||||||
|
| ``assignee`` | string | Optional | | Filter the issues |
|
||||||
|
| | | | by assignee |
|
||||||
|
+---------------+---------+--------------+---------------------------+
|
||||||
|
| ``author`` | string | Optional | | Filter the issues |
|
||||||
|
| | | | by creator |
|
||||||
|
+---------------+---------+--------------+---------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"assignee": null,
|
||||||
|
"author": null,
|
||||||
|
"status": "Closed",
|
||||||
|
"tags": [
|
||||||
|
"0.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"total_issues": 1,
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"assignee": null,
|
||||||
|
"blocks": [],
|
||||||
|
"comments": [],
|
||||||
|
"content": "asd",
|
||||||
|
"date_created": "1427442217",
|
||||||
|
"depends": [],
|
||||||
|
"id": 4,
|
||||||
|
"private": false,
|
||||||
|
"status": "Fixed",
|
||||||
|
"tags": [
|
||||||
|
"0.1"
|
||||||
|
],
|
||||||
|
"title": "bug",
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY.C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
status = flask.request.args.get('status', None)
|
||||||
|
tags = flask.request.args.getlist('tags')
|
||||||
|
tags = [tag.strip() for tag in tags if tag.strip()]
|
||||||
|
assignee = flask.request.args.get('assignee', None)
|
||||||
|
author = flask.request.args.get('author', None)
|
||||||
|
|
||||||
|
# Hide private tickets
|
||||||
|
private = False
|
||||||
|
# If user is authenticated, show him/her his/her private tickets
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
private = flask.g.fas_user.username
|
||||||
|
# If user is repo admin, show all tickets included the private ones
|
||||||
|
if is_repo_admin(repo):
|
||||||
|
private = None
|
||||||
|
|
||||||
|
if status is not None:
|
||||||
|
params = {
|
||||||
|
'session': SESSION,
|
||||||
|
'repo': repo,
|
||||||
|
'tags': tags,
|
||||||
|
'assignee': assignee,
|
||||||
|
'author': author,
|
||||||
|
'private': private
|
||||||
|
}
|
||||||
|
if status.lower() == 'closed':
|
||||||
|
params.update({'closed': True})
|
||||||
|
elif status.lower() != 'all':
|
||||||
|
params.update({'status': status})
|
||||||
|
issues = pagure.lib.search_issues(**params)
|
||||||
|
else:
|
||||||
|
issues = pagure.lib.search_issues(
|
||||||
|
SESSION, repo, status='Open', tags=tags, assignee=assignee,
|
||||||
|
author=author, private=private)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify({
|
||||||
|
'total_issues': len(issues),
|
||||||
|
'issues': [issue.to_json(public=True) for issue in issues],
|
||||||
|
'args': {
|
||||||
|
'status': status,
|
||||||
|
'tags': tags,
|
||||||
|
'assignee': assignee,
|
||||||
|
'author': author,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issue/<issueid>')
|
||||||
|
@API.route('/fork/<username>/<repo>/issue/<issueid>')
|
||||||
|
@api_login_optional()
|
||||||
|
@api_method
|
||||||
|
def api_view_issue(repo, issueid, username=None):
|
||||||
|
"""
|
||||||
|
Issue information
|
||||||
|
-----------------
|
||||||
|
Retrieve information of a specific issue.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/issue/<issue id>
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/issue/<issue id>
|
||||||
|
|
||||||
|
The identifier provided can be either the unique identifier or the
|
||||||
|
regular identifier used in the UI (for example ``24`` in
|
||||||
|
``/forks/user/test/issue/24``)
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"assignee": null,
|
||||||
|
"blocks": [],
|
||||||
|
"comments": [],
|
||||||
|
"content": "This issue needs attention",
|
||||||
|
"date_created": "1431414800",
|
||||||
|
"depends": [],
|
||||||
|
"id": 1,
|
||||||
|
"private": false,
|
||||||
|
"status": "Open",
|
||||||
|
"tags": [],
|
||||||
|
"title": "test issue",
|
||||||
|
"user": {
|
||||||
|
"fullname": "PY C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
comments = flask.request.args.get('comments', True)
|
||||||
|
if str(comments).lower() in ['0', 'False']:
|
||||||
|
comments = False
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
issue_id = issue_uid = None
|
||||||
|
try:
|
||||||
|
issue_id = int(issueid)
|
||||||
|
except:
|
||||||
|
issue_uid = issueid
|
||||||
|
|
||||||
|
issue = pagure.lib.search_issues(
|
||||||
|
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
|
||||||
|
|
||||||
|
if issue is None or issue.project != repo:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||||
|
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
if issue.private and not is_repo_admin(repo) \
|
||||||
|
and (not api_authenticated() or
|
||||||
|
not issue.user.user == flask.g.fas_user.username):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(
|
||||||
|
issue.to_json(public=True, with_comments=comments))
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issue/<issueid>/comment/<int:commentid>')
|
||||||
|
@API.route('/fork/<username>/<repo>/issue/<issueid>/comment/<int:commentid>')
|
||||||
|
@api_login_optional()
|
||||||
|
@api_method
|
||||||
|
def api_view_issue_comment(repo, issueid, commentid, username=None):
|
||||||
|
"""
|
||||||
|
Comment of an issue
|
||||||
|
--------------------
|
||||||
|
Retrieve a specific comment of an issue.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/issue/<issue id>/comment/<comment id>
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/issue/<issue id>/comment/<comment id>
|
||||||
|
|
||||||
|
The identifier provided can be either the unique identifier or the
|
||||||
|
regular identifier used in the UI (for example ``24`` in
|
||||||
|
``/forks/user/test/issue/24``)
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"avatar_url": "https://seccdn.libravatar.org/avatar/...",
|
||||||
|
"comment": "9",
|
||||||
|
"comment_date": "2015-07-01 15:08",
|
||||||
|
"date_created": "1435756127",
|
||||||
|
"id": 464,
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "P.-Y.C.",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
issue_id = issue_uid = None
|
||||||
|
try:
|
||||||
|
issue_id = int(issueid)
|
||||||
|
except:
|
||||||
|
issue_uid = issueid
|
||||||
|
|
||||||
|
issue = pagure.lib.search_issues(
|
||||||
|
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
|
||||||
|
|
||||||
|
if issue is None or issue.project != repo:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||||
|
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
if issue.private and not is_repo_admin(issue.project) \
|
||||||
|
and (not api_authenticated() or
|
||||||
|
not issue.user.user == flask.g.fas_user.username):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||||
|
|
||||||
|
comment = pagure.lib.get_issue_comment(SESSION, issue.uid, commentid)
|
||||||
|
if not comment:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ENOCOMMENT)
|
||||||
|
|
||||||
|
output = comment.to_json(public=True)
|
||||||
|
output['avatar_url'] = pagure.lib.avatar_url_from_openid(
|
||||||
|
comment.user.default_email, size=16)
|
||||||
|
output['comment_date'] = comment.date_created.strftime(
|
||||||
|
'%Y-%m-%d %H:%M:%S')
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issue/<int:issueid>/status', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/status', methods=['POST'])
|
||||||
|
@api_login_required(acls=['issue_change_status'])
|
||||||
|
@api_method
|
||||||
|
def api_change_status_issue(repo, issueid, username=None):
|
||||||
|
"""
|
||||||
|
Change issue status
|
||||||
|
-------------------
|
||||||
|
Change the status of an issue.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/issue/<issue id>/status
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/issue/<issue id>/status
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+-------------+---------+--------------+------------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+=============+=========+==============+==============================+
|
||||||
|
| ``status`` | string | Mandatory | The new status of the issue |
|
||||||
|
+-------------+---------+--------------+------------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Successfully edited issue #1"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||||
|
|
||||||
|
if issue is None or issue.project != repo:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||||
|
|
||||||
|
if issue.private and not is_repo_admin(repo) \
|
||||||
|
and (not api_authenticated() or
|
||||||
|
not issue.user.user == flask.g.fas_user.username):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||||
|
|
||||||
|
status = pagure.lib.get_issue_statuses(SESSION)
|
||||||
|
form = pagure.forms.StatusForm(status=status, csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
new_status = form.status.data
|
||||||
|
try:
|
||||||
|
# Update status
|
||||||
|
message = pagure.lib.edit_issue(
|
||||||
|
SESSION,
|
||||||
|
issue=issue,
|
||||||
|
status=new_status,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
if message:
|
||||||
|
output['message'] = message
|
||||||
|
else:
|
||||||
|
output['message'] = 'No changes'
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
SESSION.rollback()
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issue/<int:issueid>/comment', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/comment', methods=['POST'])
|
||||||
|
@api_login_required(acls=['issue_comment'])
|
||||||
|
@api_method
|
||||||
|
def api_comment_issue(repo, issueid, username=None):
|
||||||
|
"""
|
||||||
|
Comment to an issue
|
||||||
|
-------------------
|
||||||
|
Add a comment to an issue.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/issue/<issue id>/comment
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/issue/<issue id>/comment
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+--------------+----------+---------------+---------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+==============+==========+===============+===========================+
|
||||||
|
| ``comment`` | string | Mandatory | | The comment to add to |
|
||||||
|
| | | | the issue |
|
||||||
|
+--------------+----------+---------------+---------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Comment added"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||||
|
|
||||||
|
if issue is None or issue.project != repo:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||||
|
|
||||||
|
if issue.private and not is_repo_admin(repo) \
|
||||||
|
and (not api_authenticated() or
|
||||||
|
not issue.user.user == flask.g.fas_user.username):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||||
|
|
||||||
|
form = pagure.forms.CommentForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
comment = form.comment.data
|
||||||
|
try:
|
||||||
|
# New comment
|
||||||
|
message = pagure.lib.add_issue_comment(
|
||||||
|
SESSION,
|
||||||
|
issue=issue,
|
||||||
|
comment=comment,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = message
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
SESSION.rollback()
|
||||||
|
APP.logger.exception(err)
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/issue/<int:issueid>/assign', methods=['POST'])
|
||||||
|
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/assign', methods=['POST'])
|
||||||
|
@api_login_required(acls=['issue_assign'])
|
||||||
|
@api_method
|
||||||
|
def api_assign_issue(repo, issueid, username=None):
|
||||||
|
"""
|
||||||
|
Assign an issue
|
||||||
|
---------------
|
||||||
|
Assign an issue to someone.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/issue/<issue id>/assign
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/fork/<username>/<repo>/issue/<issue id>/assign
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+--------------+----------+---------------+---------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+==============+==========+===============+===========================+
|
||||||
|
| ``assignee`` | string | Mandatory | | The username of the user|
|
||||||
|
| | | | to assign the issue to. |
|
||||||
|
+--------------+----------+---------------+---------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Issue assigned"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
if not repo.settings.get('issue_tracker', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||||
|
|
||||||
|
if api_authenticated():
|
||||||
|
if repo != flask.g.token.project:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
401, error_code=APIERROR.EINVALIDTOK)
|
||||||
|
|
||||||
|
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||||
|
|
||||||
|
if issue is None or issue.project != repo:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||||
|
|
||||||
|
if issue.private and not is_repo_admin(repo) \
|
||||||
|
and (not api_authenticated() or
|
||||||
|
not issue.user.user == flask.g.fas_user.username):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||||
|
|
||||||
|
form = pagure.forms.AssignIssueForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
assignee = form.assignee.data
|
||||||
|
try:
|
||||||
|
# New comment
|
||||||
|
message = pagure.lib.add_issue_assignee(
|
||||||
|
SESSION,
|
||||||
|
issue=issue,
|
||||||
|
assignee=assignee,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
output['message'] = message
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
SESSION.rollback()
|
||||||
|
APP.logger.exception(err)
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
346
pagure/api/project.py
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.exceptions
|
||||||
|
import pagure.lib
|
||||||
|
from pagure import SESSION, APP
|
||||||
|
from pagure.api import API, api_method, APIERROR, api_login_required
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/<repo>/git/tags')
|
||||||
|
@API.route('/fork/<username>/<repo>/git/tags')
|
||||||
|
@api_method
|
||||||
|
def api_git_tags(repo, username=None):
|
||||||
|
"""
|
||||||
|
Project git tags
|
||||||
|
----------------
|
||||||
|
List the tags made on the project Git repository.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/<repo>/git/tags
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/fork/<username>/<repo>/git/tags
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"total_tags": 2,
|
||||||
|
"tags": ["0.0.1", "0.0.2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
tags = pagure.lib.git.get_git_tags(repo)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify({
|
||||||
|
'total_tags': len(tags),
|
||||||
|
'tags': tags
|
||||||
|
})
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/projects')
|
||||||
|
@api_method
|
||||||
|
def api_projects():
|
||||||
|
"""
|
||||||
|
List projects
|
||||||
|
--------------
|
||||||
|
Search projects given the specified criterias.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/projects
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/projects?tags=fedora-infra
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+===============+==========+===============+==========================+
|
||||||
|
| ``tags`` | string | Optional | | Filters the projects |
|
||||||
|
| | | | returned by their tags |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| ``pattern`` | string | Optional | | Filters the projects |
|
||||||
|
| | | | by the pattern string |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| ``username`` | string | Optional | | Filters the projects |
|
||||||
|
| | | | returned by the users |
|
||||||
|
| | | | having commit rights |
|
||||||
|
| | | | to it |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
| ``fork`` | boolean | Optional | | Filters the projects |
|
||||||
|
| | | | returned depending if |
|
||||||
|
| | | | they are forks or not |
|
||||||
|
+---------------+----------+---------------+--------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"total_projects": 2,
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"date_created": "1427441537",
|
||||||
|
"description": "A web-based calendar for Fedora",
|
||||||
|
"id": 7,
|
||||||
|
"name": "fedocal",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "Pierre-Yves C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date_created": "1431666007",
|
||||||
|
"description": "An awesome messaging servicefor everyone",
|
||||||
|
"id": 12,
|
||||||
|
"name": "fedmsg",
|
||||||
|
"parent": {
|
||||||
|
"date_created": "1433423298",
|
||||||
|
"description": "An awesome messaging servicefor everyone",
|
||||||
|
"id": 11,
|
||||||
|
"name": "fedmsg",
|
||||||
|
"parent": null,
|
||||||
|
"user": {
|
||||||
|
"fullname": "Ralph B",
|
||||||
|
"name": "ralph"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"fullname": "Pierre-Yves C",
|
||||||
|
"name": "pingou"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
tags = flask.request.values.getlist('tags')
|
||||||
|
username = flask.request.values.get('username', None)
|
||||||
|
fork = flask.request.values.get('fork', None)
|
||||||
|
pattern = flask.request.values.get('pattern', None)
|
||||||
|
|
||||||
|
if str(fork).lower() in ['1', 'true']:
|
||||||
|
fork = True
|
||||||
|
elif str(fork).lower() in ['0', 'false']:
|
||||||
|
fork = False
|
||||||
|
|
||||||
|
projects = pagure.lib.search_projects(
|
||||||
|
SESSION, username=username, fork=fork, tags=tags, pattern=pattern)
|
||||||
|
|
||||||
|
if not projects:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ENOPROJECTS)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify({
|
||||||
|
'total_projects': len(projects),
|
||||||
|
'projects': [p.to_json(api=True, public=True) for p in projects]
|
||||||
|
})
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/new/', methods=['POST'])
|
||||||
|
@API.route('/new', methods=['POST'])
|
||||||
|
@api_login_required(acls=['create_project'])
|
||||||
|
@api_method
|
||||||
|
def api_new_project():
|
||||||
|
"""
|
||||||
|
Create a new project
|
||||||
|
--------------------
|
||||||
|
Create a new project on this pagure instance.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/new
|
||||||
|
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+==================+=========+==============+===========================+
|
||||||
|
| ``name`` | string | Mandatory | | The name of the new |
|
||||||
|
| | | | project. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| ``description`` | string | Mandatory | | A short description of |
|
||||||
|
| | | | the new project. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| ``url`` | string | Optional | | An url providing more |
|
||||||
|
| | | | information about the |
|
||||||
|
| | | | project. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| ``avatar_email`` | string | Optional | | An email address for the|
|
||||||
|
| | | | avatar of the project. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| ``create_readme``| boolean | Optional | | A boolean to specify if |
|
||||||
|
| | | | there should be a readme|
|
||||||
|
| | | | added to the project on |
|
||||||
|
| | | | creation. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
'message': 'Project "foo" created'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
user = pagure.lib.search_user(SESSION, username=flask.g.fas_user.username)
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
if not pagure.APP.config.get('ENABLE_NEW_PROJECTS', True):
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ENEWPROJECTDISABLED)
|
||||||
|
|
||||||
|
form = pagure.forms.ProjectForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
name = form.name.data
|
||||||
|
description = form.description.data
|
||||||
|
url = form.url.data
|
||||||
|
avatar_email = form.avatar_email.data
|
||||||
|
create_readme = form.create_readme.data
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = pagure.lib.new_project(
|
||||||
|
SESSION,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
url=url,
|
||||||
|
avatar_email=avatar_email,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
blacklist=APP.config['BLACKLISTED_PROJECTS'],
|
||||||
|
allowed_prefix=APP.config['ALLOWED_PREFIX'],
|
||||||
|
gitfolder=APP.config['GIT_FOLDER'],
|
||||||
|
docfolder=APP.config['DOCS_FOLDER'],
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||||
|
add_readme=create_readme,
|
||||||
|
userobj=user,
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
pagure.lib.git.generate_gitolite_acls()
|
||||||
|
output['message'] = message
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
APP.logger.exception(err)
|
||||||
|
SESSION.rollback()
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/fork/', methods=['POST'])
|
||||||
|
@API.route('/fork', methods=['POST'])
|
||||||
|
@api_login_required(acls=['fork_project'])
|
||||||
|
@api_method
|
||||||
|
def api_fork_project():
|
||||||
|
"""
|
||||||
|
Fork a project
|
||||||
|
--------------------
|
||||||
|
Fork a project on this pagure instance.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /api/0/<repo>/fork
|
||||||
|
|
||||||
|
|
||||||
|
Input
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| Key | Type | Optionality | Description |
|
||||||
|
+==================+=========+==============+===========================+
|
||||||
|
| ``repo`` | string | Mandatory | | The name of the project |
|
||||||
|
| | | | to fork. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
| ``username`` | string | Optional | | The username of the user|
|
||||||
|
| | | | of the fork. |
|
||||||
|
+------------------+---------+--------------+---------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": 'Repo "test" cloned to "pingou/test"'
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
form = pagure.forms.ForkRepoForm(csrf_enabled=False)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
repo = form.repo.data
|
||||||
|
username = form.username.data or None
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
if repo is None:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
404, error_code=APIERROR.ENOPROJECT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = pagure.lib.fork_project(
|
||||||
|
SESSION,
|
||||||
|
user=flask.g.fas_user.username,
|
||||||
|
repo=repo,
|
||||||
|
gitfolder=APP.config['GIT_FOLDER'],
|
||||||
|
docfolder=APP.config['DOCS_FOLDER'],
|
||||||
|
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||||
|
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||||
|
)
|
||||||
|
SESSION.commit()
|
||||||
|
pagure.lib.git.generate_gitolite_acls()
|
||||||
|
output['message'] = message
|
||||||
|
except pagure.exceptions.PagureException as err:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||||
|
except SQLAlchemyError as err: # pragma: no cover
|
||||||
|
APP.logger.exception(err)
|
||||||
|
SESSION.rollback()
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.EDBERROR)
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.APIError(
|
||||||
|
400, error_code=APIERROR.EINVALIDREQ)
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
return jsonout
|
110
pagure/api/user.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.exceptions
|
||||||
|
import pagure.lib
|
||||||
|
from pagure import APP, SESSION
|
||||||
|
from pagure.api import API, api_method, APIERROR
|
||||||
|
|
||||||
|
|
||||||
|
@API.route('/user/<username>')
|
||||||
|
@api_method
|
||||||
|
def api_view_user(username):
|
||||||
|
"""
|
||||||
|
User information
|
||||||
|
----------------
|
||||||
|
Use this endpoint to retrieve information about a specific user.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/user/<username>
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /api/0/user/ralph
|
||||||
|
|
||||||
|
Sample response
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"forks": [],
|
||||||
|
"repos": [
|
||||||
|
{
|
||||||
|
"date_created": "1426595173",
|
||||||
|
"description": "",
|
||||||
|
"id": 5,
|
||||||
|
"name": "pagure",
|
||||||
|
"parent": null,
|
||||||
|
"settings": {
|
||||||
|
"Minimum_score_to_merge_pull-request": -1,
|
||||||
|
"Only_assignee_can_merge_pull-request": false,
|
||||||
|
"Web-hooks": null,
|
||||||
|
"issue_tracker": true,
|
||||||
|
"project_documentation": true,
|
||||||
|
"pull_requests": true
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"fullname": "ralph",
|
||||||
|
"name": "ralph"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user": {
|
||||||
|
"fullname": "ralph",
|
||||||
|
"name": "ralph"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
httpcode = 200
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
user = pagure.lib.search_user(SESSION, username=username)
|
||||||
|
if not user:
|
||||||
|
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER)
|
||||||
|
|
||||||
|
repopage = flask.request.args.get('repopage', 1)
|
||||||
|
try:
|
||||||
|
repopage = int(repopage)
|
||||||
|
except ValueError:
|
||||||
|
repopage = 1
|
||||||
|
|
||||||
|
forkpage = flask.request.args.get('forkpage', 1)
|
||||||
|
try:
|
||||||
|
forkpage = int(forkpage)
|
||||||
|
except ValueError:
|
||||||
|
forkpage = 1
|
||||||
|
|
||||||
|
limit = APP.config['ITEM_PER_PAGE']
|
||||||
|
repo_start = limit * (repopage - 1)
|
||||||
|
fork_start = limit * (forkpage - 1)
|
||||||
|
|
||||||
|
repos = pagure.lib.search_projects(
|
||||||
|
SESSION,
|
||||||
|
username=username,
|
||||||
|
fork=False)
|
||||||
|
|
||||||
|
forks = pagure.lib.search_projects(
|
||||||
|
SESSION,
|
||||||
|
username=username,
|
||||||
|
fork=True)
|
||||||
|
|
||||||
|
output['user'] = user.to_json(public=True)
|
||||||
|
output['repos'] = [repo.to_json(public=True) for repo in repos]
|
||||||
|
output['forks'] = [repo.to_json(public=True) for repo in forks]
|
||||||
|
|
||||||
|
jsonout = flask.jsonify(output)
|
||||||
|
jsonout.status_code = httpcode
|
||||||
|
return jsonout
|
219
pagure/default_config.py
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014-2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
# Set the time after which the admin session expires
|
||||||
|
ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
|
||||||
|
|
||||||
|
# secret key used to generate unique csrf token
|
||||||
|
SECRET_KEY = '<insert here your own key>'
|
||||||
|
|
||||||
|
# url to the database server:
|
||||||
|
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||||
|
|
||||||
|
# url to datagrepper (optional):
|
||||||
|
#DATAGREPPER_URL = 'https://apps.fedoraproject.org/datagrepper'
|
||||||
|
#DATAGREPPER_CATEGORY = 'pagure'
|
||||||
|
|
||||||
|
# The FAS group in which the admin of pagure are
|
||||||
|
ADMIN_GROUP = 'sysadmin-main'
|
||||||
|
|
||||||
|
# Hard-code a list of users that are global admins
|
||||||
|
PAGURE_ADMIN_USERS = []
|
||||||
|
|
||||||
|
# Whether or not to send emails
|
||||||
|
EMAIL_SEND = False
|
||||||
|
|
||||||
|
# The email address to which the flask.log will send the errors (tracebacks)
|
||||||
|
EMAIL_ERROR = 'pingou@pingoured.fr'
|
||||||
|
|
||||||
|
# The URL at which the project is available.
|
||||||
|
APP_URL = 'https://pagure.org/'
|
||||||
|
|
||||||
|
|
||||||
|
# Enables / Disables tickets for project for the entire pagure instance
|
||||||
|
ENABLE_TICKETS = True
|
||||||
|
|
||||||
|
# Enables / Disables creating projects on this pagure instance
|
||||||
|
ENABLE_NEW_PROJECTS = True
|
||||||
|
|
||||||
|
# Enables / Disables deleting projects on this pagure instance
|
||||||
|
ENABLE_DEL_PROJECTS = True
|
||||||
|
|
||||||
|
# Enables / Disables managing access to the repos
|
||||||
|
ENABLE_USER_MNGT = True
|
||||||
|
|
||||||
|
# Enables / Disables managing groups via the UI
|
||||||
|
ENABLE_GROUP_MNGT = True
|
||||||
|
|
||||||
|
# Enables / Disables showing all the projects by default on the front page
|
||||||
|
SHOW_PROJECTS_INDEX = ['repos', 'myrepos', 'myforks']
|
||||||
|
|
||||||
|
# The URL to use to clone the git repositories.
|
||||||
|
GIT_URL_SSH = 'ssh://git@pagure.org/'
|
||||||
|
GIT_URL_GIT = 'git://pagure.org/'
|
||||||
|
|
||||||
|
|
||||||
|
# Number of items displayed per page
|
||||||
|
ITEM_PER_PAGE = 48
|
||||||
|
|
||||||
|
# Maximum size of the uploaded content
|
||||||
|
MAX_CONTENT_LENGTH = 4 * 1024 * 1024 # 4 megabytes
|
||||||
|
|
||||||
|
# IP addresses allowed to access the internal endpoints
|
||||||
|
IP_ALLOWED_INTERNAL = ['127.0.0.1', 'localhost', '::1']
|
||||||
|
|
||||||
|
# Redis configuration
|
||||||
|
EVENTSOURCE_SOURCE = None
|
||||||
|
WEBHOOK = False
|
||||||
|
REDIS_HOST = '0.0.0.0'
|
||||||
|
REDIS_PORT = 6379
|
||||||
|
REDIS_DB = 0
|
||||||
|
EVENTSOURCE_PORT = 8080
|
||||||
|
|
||||||
|
# Folder containing to the git repos
|
||||||
|
GIT_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'repos'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Folder containing the forks repos
|
||||||
|
FORK_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'forks'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Folder containing the docs repos
|
||||||
|
DOCS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'docs'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Folder containing the tickets repos
|
||||||
|
TICKETS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'tickets'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Folder containing the pull-requests repos
|
||||||
|
REQUESTS_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'requests'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Folder containing the clones for the remote pull-requests
|
||||||
|
REMOTE_GIT_FOLDER = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'remotes'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration file for gitolite
|
||||||
|
GITOLITE_CONFIG = os.path.join(
|
||||||
|
os.path.abspath(os.path.dirname(__file__)),
|
||||||
|
'..',
|
||||||
|
'gitolite.conf'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration keys to specify where the upload folder is and what is its
|
||||||
|
# name
|
||||||
|
UPLOAD_FOLDER = 'releases/'
|
||||||
|
UPLOAD_FOLDER_PATH = './' + UPLOAD_FOLDER
|
||||||
|
|
||||||
|
# Home folder of the gitolite user -- Folder where to run gl-compile-conf from
|
||||||
|
GITOLITE_HOME = None
|
||||||
|
|
||||||
|
# Version of gitolite used: 2 or 3?
|
||||||
|
GITOLITE_VERSION = 3
|
||||||
|
|
||||||
|
# Folder containing all the public ssh keys for gitolite
|
||||||
|
GITOLITE_KEYDIR = None
|
||||||
|
|
||||||
|
# Path to the gitolite.rc file
|
||||||
|
GL_RC = None
|
||||||
|
# Path to the /bin directory where the gitolite tools can be found
|
||||||
|
GL_BINDIR = None
|
||||||
|
|
||||||
|
|
||||||
|
#SMTP settings
|
||||||
|
SMTP_SERVER = 'localhost'
|
||||||
|
SMTP_PORT = 25
|
||||||
|
SMTP_SSL = False
|
||||||
|
|
||||||
|
# Specify both for enabling SMTP auth
|
||||||
|
SMTP_USERNAME = None
|
||||||
|
SMTP_PASSWORD = None
|
||||||
|
|
||||||
|
|
||||||
|
# Email used to sent emails
|
||||||
|
FROM_EMAIL = 'pagure@pagure.org'
|
||||||
|
|
||||||
|
DOMAIN_EMAIL_NOTIFICATIONS = 'pagure.org'
|
||||||
|
SALT_EMAIL = '<secret key to be changed>'
|
||||||
|
|
||||||
|
# Specify which authentication method to use, defaults to `fas` can be or
|
||||||
|
# `local`
|
||||||
|
# Default: ``fas``.
|
||||||
|
PAGURE_AUTH = 'fas'
|
||||||
|
|
||||||
|
# When this is set to True, the session cookie will only be returned to the
|
||||||
|
# server via ssl (https). If you connect to the server via plain http, the
|
||||||
|
# cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||||
|
# This may be set to False when testing your application but should always
|
||||||
|
# be set to True in production.
|
||||||
|
# Default: ``True``.
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
SESSION_COOKIE_NAME = 'pagure'
|
||||||
|
|
||||||
|
# Boolean specifying wether to check the user's IP address when retrieving
|
||||||
|
# its session. This make things more secure (thus is on by default) but
|
||||||
|
# under certain setup it might not work (for example is there are proxies
|
||||||
|
# in front of the application).
|
||||||
|
CHECK_SESSION_IP = True
|
||||||
|
|
||||||
|
# Lenght for short commits ids or file hex
|
||||||
|
SHORT_LENGTH = 6
|
||||||
|
|
||||||
|
# Used by SESSION_COOKIE_PATH
|
||||||
|
APPLICATION_ROOT = '/'
|
||||||
|
|
||||||
|
# List of blacklisted project names
|
||||||
|
BLACKLISTED_PROJECTS = [
|
||||||
|
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||||
|
'logout', 'login', 'users', 'groups', 'projects', 'ssh_info']
|
||||||
|
|
||||||
|
# List of prefix allowed in project names
|
||||||
|
ALLOWED_PREFIX = []
|
||||||
|
|
||||||
|
# List of blacklisted group names
|
||||||
|
BLACKLISTED_GROUPS = ['forks']
|
||||||
|
|
||||||
|
|
||||||
|
ACLS = {
|
||||||
|
'create_project': 'Create a new project',
|
||||||
|
'fork_project': 'Fork a project',
|
||||||
|
'issue_assign': 'Assign issue to someone',
|
||||||
|
'issue_create': 'Create a new ticket against this project',
|
||||||
|
'issue_change_status': 'Change the status of a ticket of this project',
|
||||||
|
'issue_comment': 'Comment on a ticket of this project',
|
||||||
|
'pull_request_close': 'Close a pull-request of this project',
|
||||||
|
'pull_request_comment': 'Comment on a pull-request of this project',
|
||||||
|
'pull_request_flag': 'Flag a pull-request of this project',
|
||||||
|
'pull_request_merge': 'Merge a pull-request of this project',
|
||||||
|
}
|
69
pagure/doc/api.rst
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
Authentication
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To access some endpoints, you need to login to Pagure using API token. You
|
||||||
|
can generate one in the project setting page.
|
||||||
|
|
||||||
|
When sending HTTP request, include an ``Authorization`` field in the header
|
||||||
|
with value ``token $your-api-token``, where ``$your-api-token`` is the
|
||||||
|
API token generated in the project setting page.
|
||||||
|
|
||||||
|
So the result should look like:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Authorization: token abcdefghijklmnop
|
||||||
|
|
||||||
|
Where ``abcdefghijklmnop`` is the API token provided by pagure.
|
||||||
|
|
||||||
|
Anyone with the token can access the APIs on your behalf, so please be
|
||||||
|
sure to keep it private and safe.
|
||||||
|
|
||||||
|
Request Encoding
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The payload of POST and GET requests is encoded as
|
||||||
|
|
||||||
|
``application/x-www-form-urlencoded``.
|
||||||
|
|
||||||
|
|
||||||
|
This is an example URL of a GET request:
|
||||||
|
|
||||||
|
``https://pagure.io/api/0/test/issues?status=Open&tags=Pagure&tags=Enhancement``
|
||||||
|
|
||||||
|
|
||||||
|
Return Encoding
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The return value of API calls is ``application/json``. This is an
|
||||||
|
example of return value:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"assignee": null,
|
||||||
|
"author": null,
|
||||||
|
"status": null,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"assignee": null,
|
||||||
|
"blocks": [],
|
||||||
|
"comments": [],
|
||||||
|
"content": "Sample ticket",
|
||||||
|
"date_created": "1434266418",
|
||||||
|
"depends": [],
|
||||||
|
"id": 4,
|
||||||
|
"private": false,
|
||||||
|
"status": "Open",
|
||||||
|
"tags": [],
|
||||||
|
"title": "This is a sample",
|
||||||
|
"user": {
|
||||||
|
"fullname": "Pagure",
|
||||||
|
"name": "API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
122
pagure/doc_utils.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Ralph Bean <rbean@redhat.com>
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import docutils
|
||||||
|
import docutils.core
|
||||||
|
import docutils.examples
|
||||||
|
import markupsafe
|
||||||
|
import markdown
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
def modify_rst(rst, view_file_url=None):
|
||||||
|
""" Downgrade some of our rst directives if docutils is too old. """
|
||||||
|
if view_file_url:
|
||||||
|
rst = rst.replace(
|
||||||
|
'.. image:: ',
|
||||||
|
'.. image:: %s' % view_file_url
|
||||||
|
)
|
||||||
|
|
||||||
|
# We catch Exception if we want :-p
|
||||||
|
# pylint: disable=W0703
|
||||||
|
try:
|
||||||
|
# The rst features we need were introduced in this version
|
||||||
|
minimum = [0, 9]
|
||||||
|
version = [int(cpt) for cpt in docutils.__version__.split('.')]
|
||||||
|
|
||||||
|
# If we're at or later than that version, no need to downgrade
|
||||||
|
if version >= minimum:
|
||||||
|
return rst
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
# If there was some error parsing or comparing versions, run the
|
||||||
|
# substitutions just to be safe.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# On Fedora this will never work as the docutils version is to recent
|
||||||
|
# Otherwise, make code-blocks into just literal blocks.
|
||||||
|
substitutions = { # pragma: no cover
|
||||||
|
'.. code-block:: javascript': '::',
|
||||||
|
}
|
||||||
|
|
||||||
|
for old, new in substitutions.items(): # pragma: no cover
|
||||||
|
rst = rst.replace(old, new)
|
||||||
|
|
||||||
|
return rst # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
def modify_html(html):
|
||||||
|
""" Perform style substitutions where docutils doesn't do what we want.
|
||||||
|
"""
|
||||||
|
|
||||||
|
substitutions = {
|
||||||
|
'<tt class="docutils literal">': '<code>',
|
||||||
|
'</tt>': '</code>',
|
||||||
|
}
|
||||||
|
for old, new in substitutions.items():
|
||||||
|
html = html.replace(old, new)
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def convert_doc(rst_string, view_file_url=None):
|
||||||
|
""" Utility to load an RST file and turn it into fancy HTML. """
|
||||||
|
rst = modify_rst(rst_string, view_file_url)
|
||||||
|
|
||||||
|
overrides = {'report_level': 'quiet'}
|
||||||
|
try:
|
||||||
|
html = docutils.core.publish_parts(
|
||||||
|
source=rst,
|
||||||
|
writer_name='html',
|
||||||
|
settings_overrides=overrides)
|
||||||
|
except:
|
||||||
|
return '<pre>%s</pre>' % rst
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
html_string = html['html_body']
|
||||||
|
|
||||||
|
html_string = modify_html(html_string)
|
||||||
|
|
||||||
|
html_string = markupsafe.Markup(html_string)
|
||||||
|
return html_string
|
||||||
|
|
||||||
|
|
||||||
|
def convert_readme(content, ext, view_file_url=None):
|
||||||
|
''' Convert the provided content according to the extension of the file
|
||||||
|
provided.
|
||||||
|
'''
|
||||||
|
output = content
|
||||||
|
safe = False
|
||||||
|
if ext and ext in ['.rst']:
|
||||||
|
safe = True
|
||||||
|
output = convert_doc(content.decode('utf-8'), view_file_url)
|
||||||
|
elif ext and ext in ['.mk', '.md', '.markdown']:
|
||||||
|
output = markdown.markdown(content.decode('utf-8'))
|
||||||
|
safe = True
|
||||||
|
elif not ext or (ext and ext in ['.text', '.txt']):
|
||||||
|
safe = True
|
||||||
|
output = '<pre>%s</pre>' % content
|
||||||
|
return output, safe
|
||||||
|
|
||||||
|
|
||||||
|
def load_doc(endpoint):
|
||||||
|
""" Utility to load an RST file and turn it into fancy HTML. """
|
||||||
|
|
||||||
|
rst = unicode(textwrap.dedent(endpoint.__doc__))
|
||||||
|
|
||||||
|
rst = modify_rst(rst)
|
||||||
|
|
||||||
|
api_docs = docutils.examples.html_body(rst)
|
||||||
|
|
||||||
|
api_docs = modify_html(api_docs)
|
||||||
|
|
||||||
|
api_docs = markupsafe.Markup(api_docs)
|
||||||
|
return api_docs
|
183
pagure/docs_server.py
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014-2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import pygit2
|
||||||
|
|
||||||
|
import pagure.doc_utils
|
||||||
|
import pagure.exceptions
|
||||||
|
import pagure.lib
|
||||||
|
import pagure.forms
|
||||||
|
|
||||||
|
# Create the application.
|
||||||
|
APP = flask.Flask(__name__)
|
||||||
|
|
||||||
|
# set up FAS
|
||||||
|
APP.config.from_object('pagure.default_config')
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' in os.environ:
|
||||||
|
APP.config.from_envvar('PAGURE_CONFIG')
|
||||||
|
|
||||||
|
SESSION = pagure.lib.create_session(APP.config['DB_URL'])
|
||||||
|
|
||||||
|
if not APP.debug:
|
||||||
|
APP.logger.addHandler(pagure.mail_logging.get_mail_handler(
|
||||||
|
smtp_server=APP.config.get('SMTP_SERVER', '127.0.0.1'),
|
||||||
|
mail_admin=APP.config.get('MAIL_ADMIN', APP.config['EMAIL_ERROR'])
|
||||||
|
))
|
||||||
|
|
||||||
|
# Send classic logs into syslog
|
||||||
|
SHANDLER = logging.StreamHandler()
|
||||||
|
SHANDLER.setLevel(APP.config.get('log_level', 'INFO'))
|
||||||
|
APP.logger.addHandler(SHANDLER)
|
||||||
|
|
||||||
|
LOG = APP.logger
|
||||||
|
|
||||||
|
TMPL_HTML = '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<style type="text/css">
|
||||||
|
ul {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{content}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def __get_tree(repo_obj, tree, filepath, index=0, extended=False):
|
||||||
|
''' Retrieve the entry corresponding to the provided filename in a
|
||||||
|
given tree.
|
||||||
|
'''
|
||||||
|
filename = filepath[index]
|
||||||
|
if isinstance(tree, pygit2.Blob): # pragma: no cover
|
||||||
|
# If we were given a blob, then let's just return it
|
||||||
|
return (tree, None, None)
|
||||||
|
|
||||||
|
for element in tree:
|
||||||
|
if element.name == filename or \
|
||||||
|
(not filename and element.name.startswith('index')):
|
||||||
|
# If we have a folder we must go one level deeper
|
||||||
|
if element.filemode == 16384:
|
||||||
|
if (index + 1) == len(filepath):
|
||||||
|
filepath.append('')
|
||||||
|
return __get_tree(
|
||||||
|
repo_obj, repo_obj[element.oid], filepath,
|
||||||
|
index=index + 1, extended=True)
|
||||||
|
else:
|
||||||
|
return (element, tree, False)
|
||||||
|
|
||||||
|
if filename == '':
|
||||||
|
return (None, tree, extended)
|
||||||
|
else:
|
||||||
|
raise pagure.exceptions.FileNotFoundException(
|
||||||
|
'File %s not found' % ('/'.join(filepath),))
|
||||||
|
|
||||||
|
|
||||||
|
def __get_tree_and_content(repo_obj, commit, path):
|
||||||
|
''' Return the tree and the content of the specified file. '''
|
||||||
|
|
||||||
|
(blob_or_tree, tree_obj, extended) = __get_tree(
|
||||||
|
repo_obj, commit.tree, path)
|
||||||
|
|
||||||
|
if blob_or_tree is None:
|
||||||
|
return (tree_obj, None, False, extended)
|
||||||
|
|
||||||
|
if not repo_obj[blob_or_tree.oid]:
|
||||||
|
# Not tested and no idea how to test it, but better safe than sorry
|
||||||
|
flask.abort(404, 'File not found')
|
||||||
|
|
||||||
|
if isinstance(blob_or_tree, pygit2.TreeEntry): # Returned a file
|
||||||
|
ext = os.path.splitext(blob_or_tree.name)[1]
|
||||||
|
blob_obj = repo_obj[blob_or_tree.oid]
|
||||||
|
content, safe = pagure.doc_utils.convert_readme(blob_obj.data, ext)
|
||||||
|
|
||||||
|
tree = sorted(tree_obj, key=lambda x: x.filemode)
|
||||||
|
return (tree, content, safe, extended)
|
||||||
|
|
||||||
|
|
||||||
|
@APP.route('/<repo>/')
|
||||||
|
@APP.route('/<repo>/<path:filename>')
|
||||||
|
@APP.route('/fork/<username>/<repo>/')
|
||||||
|
@APP.route('/fork/<username>/<repo>/<path:filename>')
|
||||||
|
def view_docs(repo, username=None, filename=None):
|
||||||
|
""" Display the documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||||
|
|
||||||
|
if not repo:
|
||||||
|
flask.abort(404, 'Project not found')
|
||||||
|
|
||||||
|
if not repo.settings.get('project_documentation', True):
|
||||||
|
flask.abort(404, 'This project has documentation disabled')
|
||||||
|
|
||||||
|
reponame = os.path.join(APP.config['DOCS_FOLDER'], repo.path)
|
||||||
|
if not os.path.exists(reponame):
|
||||||
|
flask.abort(404, 'Documentation not found')
|
||||||
|
|
||||||
|
repo_obj = pygit2.Repository(reponame)
|
||||||
|
|
||||||
|
|
||||||
|
if not repo_obj.is_empty:
|
||||||
|
commit = repo_obj[repo_obj.head.target]
|
||||||
|
else:
|
||||||
|
flask.abort(404, 'No content found is the repository')
|
||||||
|
branchname = 'master'
|
||||||
|
|
||||||
|
content = None
|
||||||
|
tree = None
|
||||||
|
safe = False
|
||||||
|
if not filename:
|
||||||
|
path = ['']
|
||||||
|
else:
|
||||||
|
path = [it for it in filename.split('/') if it]
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
try:
|
||||||
|
(tree, content, safe, extended) = __get_tree_and_content(
|
||||||
|
repo_obj, commit, path)
|
||||||
|
if extended:
|
||||||
|
filename += '/'
|
||||||
|
except pagure.exceptions.FileNotFoundException as err:
|
||||||
|
flask.flash(err.message, 'error')
|
||||||
|
|
||||||
|
mimetype = None
|
||||||
|
if not filename:
|
||||||
|
pass
|
||||||
|
elif filename.endswith('.css'):
|
||||||
|
mimetype = 'text/css'
|
||||||
|
elif filename.endswith('.js'):
|
||||||
|
mimetype = 'application/javascript'
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
if not tree or not len(tree):
|
||||||
|
flask.abort(404, 'No content found is the repository')
|
||||||
|
html = '<li>'
|
||||||
|
for el in tree:
|
||||||
|
name = el.name
|
||||||
|
# Append a trailing '/' to the folders
|
||||||
|
if el.filemode == 16384:
|
||||||
|
name += '/'
|
||||||
|
html += '<ul><a href="{0}">{1}</a></ul>'.format(el.name, name)
|
||||||
|
html += '</li>'
|
||||||
|
content = TMPL_HTML.format(content=html)
|
||||||
|
|
||||||
|
return flask.Response(content, mimetype=mimetype)
|
59
pagure/exceptions.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PagureException(Exception):
|
||||||
|
''' Parent class of all the exception for all Pagure specific
|
||||||
|
exceptions.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RepoExistsException(PagureException):
|
||||||
|
''' Exception thrown when trying to create a repository that already
|
||||||
|
exists.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotFoundException(PagureException):
|
||||||
|
''' Exception thrown when trying to create a repository that already
|
||||||
|
exists.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(PagureException):
|
||||||
|
''' Exception raised by the API when something goes wrong. '''
|
||||||
|
|
||||||
|
def __init__(self, status_code, error_code, error=None):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.error_code = error_code
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
|
||||||
|
class BranchNotFoundException(PagureException):
|
||||||
|
''' Exception thrown when trying to use a branch that could not be
|
||||||
|
found in a repository.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PagureEvException(PagureException):
|
||||||
|
''' Exceptions used in the pagure-stream-server.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GitConflictsException(PagureException):
|
||||||
|
''' Exception used when trying to pull on a repo and that leads to
|
||||||
|
conflicts.
|
||||||
|
'''
|
||||||
|
pass
|
423
pagure/forms.py
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from flask.ext import wtf
|
||||||
|
import wtforms
|
||||||
|
# pylint: disable=R0903,W0232,E1002
|
||||||
|
|
||||||
|
|
||||||
|
STRICT_REGEX = '^[a-zA-Z0-9-_]+$'
|
||||||
|
TAGS_REGEX = '^[a-zA-Z0-9-_, .]+$'
|
||||||
|
PROJECT_NAME_REGEX = \
|
||||||
|
'^[a-zA-z0-9_][a-zA-Z0-9-_]*(/?[a-zA-z0-9_][a-zA-Z0-9-_]+)?$'
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectFormSimplified(wtf.Form):
|
||||||
|
''' Form to edit the description of a project. '''
|
||||||
|
description = wtforms.TextField(
|
||||||
|
'Description <span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
url = wtforms.TextField(
|
||||||
|
'URL',
|
||||||
|
[wtforms.validators.optional()]
|
||||||
|
)
|
||||||
|
avatar_email = wtforms.TextField(
|
||||||
|
'Avatar email',
|
||||||
|
[wtforms.validators.optional()]
|
||||||
|
)
|
||||||
|
tags = wtforms.TextField(
|
||||||
|
'Project tags',
|
||||||
|
[wtforms.validators.optional()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectForm(ProjectFormSimplified):
|
||||||
|
''' Form to create or edit project. '''
|
||||||
|
name = wtforms.TextField(
|
||||||
|
'Project name <span class="error">*</span>',
|
||||||
|
[
|
||||||
|
wtforms.validators.Required(),
|
||||||
|
wtforms.validators.Regexp(PROJECT_NAME_REGEX, flags=re.IGNORECASE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
create_readme = wtforms.BooleanField(
|
||||||
|
'Create README',
|
||||||
|
[wtforms.validators.optional()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueFormSimplied(wtf.Form):
|
||||||
|
''' Form to create or edit an issue. '''
|
||||||
|
title = wtforms.TextField(
|
||||||
|
'Title<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
issue_content = wtforms.TextAreaField(
|
||||||
|
'Content<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
private = wtforms.BooleanField(
|
||||||
|
'Private',
|
||||||
|
[wtforms.validators.optional()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueForm(IssueFormSimplied):
|
||||||
|
''' Form to create or edit an issue. '''
|
||||||
|
status = wtforms.SelectField(
|
||||||
|
'Status',
|
||||||
|
[wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(IssueForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'status' in kwargs:
|
||||||
|
self.status.choices = [
|
||||||
|
(status, status) for status in kwargs['status']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RequestPullForm(wtf.Form):
|
||||||
|
''' Form to create a request pull. '''
|
||||||
|
title = wtforms.TextField(
|
||||||
|
'Title<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
initial_comment = wtforms.TextAreaField(
|
||||||
|
'Initial Comment', [wtforms.validators.Optional()])
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteRequestPullForm(RequestPullForm):
|
||||||
|
''' Form to create a remote request pull. '''
|
||||||
|
git_repo = wtforms.TextField(
|
||||||
|
'Git repo address<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
branch_from = wtforms.TextField(
|
||||||
|
'Git branch<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
branch_to = wtforms.TextField(
|
||||||
|
'Git branch to merge in<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddIssueTagForm(wtf.Form):
|
||||||
|
''' Form to add a comment to an issue. '''
|
||||||
|
tag = wtforms.TextField(
|
||||||
|
'tag',
|
||||||
|
[
|
||||||
|
wtforms.validators.Optional(),
|
||||||
|
wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StatusForm(wtf.Form):
|
||||||
|
''' Form to add/change the status of an issue. '''
|
||||||
|
status = wtforms.SelectField(
|
||||||
|
'Status',
|
||||||
|
[wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(StatusForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'status' in kwargs:
|
||||||
|
self.status.choices = [
|
||||||
|
(status, status) for status in kwargs['status']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NewTokenForm(wtf.Form):
|
||||||
|
''' Form to add/change the status of an issue. '''
|
||||||
|
acls = wtforms.SelectMultipleField(
|
||||||
|
'ACLs',
|
||||||
|
[wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(NewTokenForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'acls' in kwargs:
|
||||||
|
self.acls.choices = [
|
||||||
|
(acl.name, acl.name) for acl in kwargs['acls']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateIssueForm(wtf.Form):
|
||||||
|
''' Form to add a comment to an issue. '''
|
||||||
|
tag = wtforms.TextField(
|
||||||
|
'tag',
|
||||||
|
[
|
||||||
|
wtforms.validators.Optional(),
|
||||||
|
wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
depends = wtforms.TextField(
|
||||||
|
'dependency issue', [wtforms.validators.Optional()]
|
||||||
|
)
|
||||||
|
blocks = wtforms.TextField(
|
||||||
|
'blocked issue', [wtforms.validators.Optional()]
|
||||||
|
)
|
||||||
|
comment = wtforms.TextAreaField(
|
||||||
|
'Comment', [wtforms.validators.Optional()]
|
||||||
|
)
|
||||||
|
assignee = wtforms.TextAreaField(
|
||||||
|
'Assigned to', [wtforms.validators.Optional()]
|
||||||
|
)
|
||||||
|
status = wtforms.SelectField(
|
||||||
|
'Status',
|
||||||
|
[wtforms.validators.Optional()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
priority = wtforms.SelectField(
|
||||||
|
'Priority',
|
||||||
|
[wtforms.validators.Optional()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(UpdateIssueForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'status' in kwargs:
|
||||||
|
self.status.choices = [
|
||||||
|
(status, status) for status in kwargs['status']
|
||||||
|
]
|
||||||
|
|
||||||
|
self.priority.choices = []
|
||||||
|
if 'priorities' in kwargs:
|
||||||
|
for key in sorted(kwargs['priorities']):
|
||||||
|
self.priority.choices.append(
|
||||||
|
(key, kwargs['priorities'][key])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddPullRequestCommentForm(wtf.Form):
|
||||||
|
''' Form to add a comment to a pull-request. '''
|
||||||
|
commit = wtforms.HiddenField('commit identifier')
|
||||||
|
filename = wtforms.HiddenField('file changed')
|
||||||
|
row = wtforms.HiddenField('row')
|
||||||
|
requestid = wtforms.HiddenField('requestid')
|
||||||
|
tree_id = wtforms.HiddenField('treeid')
|
||||||
|
comment = wtforms.TextAreaField(
|
||||||
|
'Comment<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddPullRequestFlagForm(wtf.Form):
|
||||||
|
''' Form to add a flag to a pull-request. '''
|
||||||
|
username = wtforms.TextField(
|
||||||
|
'Username', [wtforms.validators.Required()])
|
||||||
|
percent = wtforms.TextField(
|
||||||
|
'Percentage of completion', [wtforms.validators.Required()])
|
||||||
|
comment = wtforms.TextAreaField(
|
||||||
|
'Comment', [wtforms.validators.Required()])
|
||||||
|
url = wtforms.TextField(
|
||||||
|
'URL', [wtforms.validators.Required()])
|
||||||
|
uid = wtforms.TextField(
|
||||||
|
'UID', [wtforms.validators.optional()])
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsForm(wtf.Form):
|
||||||
|
''' Form to create or edit project. '''
|
||||||
|
ssh_key = wtforms.TextAreaField(
|
||||||
|
'Public SSH key <span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddUserForm(wtf.Form):
|
||||||
|
''' Form to add a user to a project. '''
|
||||||
|
user = wtforms.TextField(
|
||||||
|
'Username <span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AssignIssueForm(wtf.Form):
|
||||||
|
''' Form to assign an user to an issue. '''
|
||||||
|
assignee = wtforms.TextField(
|
||||||
|
'Assignee <span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddGroupForm(wtf.Form):
|
||||||
|
''' Form to add a group to a project. '''
|
||||||
|
group = wtforms.TextField(
|
||||||
|
'Group <span class="error">*</span>',
|
||||||
|
[
|
||||||
|
wtforms.validators.Required(),
|
||||||
|
wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmationForm(wtf.Form):
|
||||||
|
''' Simple form used just for CSRF protection. '''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UploadFileForm(wtf.Form):
|
||||||
|
''' Form to upload a file. '''
|
||||||
|
filestream = wtforms.FileField(
|
||||||
|
'File',
|
||||||
|
[wtforms.validators.Required()])
|
||||||
|
|
||||||
|
|
||||||
|
class UserEmailForm(wtf.Form):
|
||||||
|
''' Form to edit the description of a project. '''
|
||||||
|
email = wtforms.TextField(
|
||||||
|
'email', [wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(UserEmailForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'emails' in kwargs:
|
||||||
|
if kwargs['emails']:
|
||||||
|
self.email.validators.append(
|
||||||
|
wtforms.validators.NoneOf(kwargs['emails'])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.email.validators = [wtforms.validators.Required()]
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectCommentForm(wtf.Form):
|
||||||
|
''' Form to represent project. '''
|
||||||
|
objid = wtforms.TextField(
|
||||||
|
'Ticket/Request id',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
useremail = wtforms.TextField(
|
||||||
|
'Email',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(wtf.Form):
|
||||||
|
''' Form to upload a file. '''
|
||||||
|
comment = wtforms.FileField(
|
||||||
|
'Comment',
|
||||||
|
[wtforms.validators.Required()])
|
||||||
|
|
||||||
|
|
||||||
|
class NewGroupForm(wtf.Form):
|
||||||
|
""" Form to ask for a password change. """
|
||||||
|
group_name = wtforms.TextField(
|
||||||
|
'Group name <span class="error">*</span>',
|
||||||
|
[
|
||||||
|
wtforms.validators.Required(),
|
||||||
|
wtforms.validators.Length(max=16),
|
||||||
|
wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
group_type = wtforms.SelectField(
|
||||||
|
'Group type',
|
||||||
|
[wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(NewGroupForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'group_types' in kwargs:
|
||||||
|
self.group_type.choices = [
|
||||||
|
(grptype, grptype) for grptype in kwargs['group_types']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EditFileForm(wtf.Form):
|
||||||
|
""" Form used to edit a file. """
|
||||||
|
content = wtforms.TextAreaField(
|
||||||
|
'content', [wtforms.validators.Required()])
|
||||||
|
commit_title = wtforms.TextField(
|
||||||
|
'Title', [wtforms.validators.Required()])
|
||||||
|
commit_message = wtforms.TextAreaField(
|
||||||
|
'Commit message', [wtforms.validators.optional()])
|
||||||
|
email = wtforms.SelectField(
|
||||||
|
'Email', [wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
branch = wtforms.TextField(
|
||||||
|
'Branch', [wtforms.validators.Required()])
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(EditFileForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'emails' in kwargs:
|
||||||
|
self.email.choices = [
|
||||||
|
(email.email, email.email) for email in kwargs['emails']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultBranchForm(wtf.Form):
|
||||||
|
"""Form to change the default branh for a repository"""
|
||||||
|
branches = wtforms.SelectField(
|
||||||
|
'default_branch',
|
||||||
|
[wtforms.validators.Required()],
|
||||||
|
choices=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Calls the default constructor with the normal argument but
|
||||||
|
uses the list of collection provided to fill the choices of the
|
||||||
|
drop-down list.
|
||||||
|
"""
|
||||||
|
super(DefaultBranchForm, self).__init__(*args, **kwargs)
|
||||||
|
if 'branches' in kwargs:
|
||||||
|
self.branches.choices = [
|
||||||
|
(branch, branch) for branch in kwargs['branches']
|
||||||
|
]
|
||||||
|
|
||||||
|
class EditCommentForm(wtf.Form):
|
||||||
|
""" Form to verify that comment is not empty
|
||||||
|
"""
|
||||||
|
update_comment = wtforms.TextAreaField(
|
||||||
|
'Comment<span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ForkRepoForm(wtf.Form):
|
||||||
|
''' Form to fork a project in the API. '''
|
||||||
|
repo = wtforms.TextField(
|
||||||
|
'The project name <span class="error">*</span>',
|
||||||
|
[wtforms.validators.Required()]
|
||||||
|
)
|
||||||
|
username = wtforms.TextField(
|
||||||
|
'User who forked the project',
|
||||||
|
[wtforms.validators.optional()])
|
123
pagure/hooks/__init__.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2014 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import wtforms
|
||||||
|
|
||||||
|
from pagure.exceptions import FileNotFoundException
|
||||||
|
from pagure import APP, get_repo_path
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredIf(wtforms.validators.Required):
|
||||||
|
""" Wtforms validator setting a field as required if another field
|
||||||
|
has a value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fields, *args, **kwargs):
|
||||||
|
if isinstance(fields, basestring):
|
||||||
|
fields = [fields]
|
||||||
|
self.fields = fields
|
||||||
|
super(RequiredIf, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
for fieldname in self.fields:
|
||||||
|
nfield = form._fields.get(fieldname)
|
||||||
|
if nfield is None:
|
||||||
|
raise Exception(
|
||||||
|
'no field named "%s" in form' % fieldname)
|
||||||
|
if bool(nfield.data):
|
||||||
|
super(RequiredIf, self).__call__(form, field)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHook(object):
|
||||||
|
''' Base class for pagure's hooks. '''
|
||||||
|
|
||||||
|
name = None
|
||||||
|
form = None
|
||||||
|
description = None
|
||||||
|
hook_type = 'post-receive'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_up(cls, project):
|
||||||
|
''' Install the generic post-receive hook that allow us to call
|
||||||
|
multiple post-receive hooks as set per plugin.
|
||||||
|
'''
|
||||||
|
repopaths = [get_repo_path(project)]
|
||||||
|
for folder in [
|
||||||
|
APP.config.get('DOCS_FOLDER'),
|
||||||
|
APP.config.get('REQUESTS_FOLDER')]:
|
||||||
|
repopaths.append(
|
||||||
|
os.path.join(folder, project.path)
|
||||||
|
)
|
||||||
|
|
||||||
|
hook_files = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)), 'files')
|
||||||
|
|
||||||
|
for repopath in repopaths:
|
||||||
|
# Make sure the hooks folder exists
|
||||||
|
hookfolder = os.path.join(repopath, 'hooks')
|
||||||
|
if not os.path.exists(hookfolder):
|
||||||
|
os.makedirs(hookfolder)
|
||||||
|
|
||||||
|
# Install the main post-receive file
|
||||||
|
postreceive = os.path.join(hookfolder, cls.hook_type)
|
||||||
|
if not os.path.exists(postreceive):
|
||||||
|
os.symlink(os.path.join(hook_files, cls.hook_type),
|
||||||
|
postreceive)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def base_install(cls, repopaths, dbobj, hook_name, filein):
|
||||||
|
''' Method called to install the hook for a project.
|
||||||
|
|
||||||
|
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||||
|
should be installed
|
||||||
|
:arg dbobj: the DB object the hook uses to store the settings
|
||||||
|
information.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for repopath in repopaths:
|
||||||
|
if not os.path.exists(repopath):
|
||||||
|
raise FileNotFoundException('Repo %s not found' % repopath)
|
||||||
|
|
||||||
|
hook_files = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)), 'files')
|
||||||
|
|
||||||
|
# Make sure the hooks folder exists
|
||||||
|
hookfolder = os.path.join(repopath, 'hooks')
|
||||||
|
if not os.path.exists(hookfolder):
|
||||||
|
os.makedirs(hookfolder)
|
||||||
|
|
||||||
|
# Install the hook itself
|
||||||
|
hook_file = os.path.join(repopath, 'hooks', cls.hook_type + '.'
|
||||||
|
+ hook_name)
|
||||||
|
|
||||||
|
if not os.path.exists(hook_file):
|
||||||
|
os.symlink(
|
||||||
|
os.path.join(hook_files, filein),
|
||||||
|
hook_file
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def base_remove(cls, repopaths, hook_name):
|
||||||
|
''' Method called to remove the hook of a project.
|
||||||
|
|
||||||
|
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||||
|
should be installed
|
||||||
|
|
||||||
|
'''
|
||||||
|
for repopath in repopaths:
|
||||||
|
if not os.path.exists(repopath):
|
||||||
|
raise FileNotFoundException('Repo %s not found' % repopath)
|
||||||
|
|
||||||
|
hook_path = os.path.join(repopath, 'hooks', cls.hook_type + '.'
|
||||||
|
+ hook_name)
|
||||||
|
if os.path.exists(hook_path):
|
||||||
|
os.unlink(hook_path)
|
90
pagure/hooks/fedmsg.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
(c) 2015 - Copyright Red Hat Inc
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import wtforms
|
||||||
|
from flask.ext import wtf
|
||||||
|
from sqlalchemy.orm import relation
|
||||||
|
from sqlalchemy.orm import backref
|
||||||
|
|
||||||
|
from pagure.hooks import BaseHook
|
||||||
|
from pagure.lib.model import BASE, Project
|
||||||
|
from pagure import get_repo_path
|
||||||
|
|
||||||
|
|
||||||
|
class FedmsgTable(BASE):
|
||||||
|
""" Stores information about the fedmsg hook deployed on a project.
|
||||||
|
|
||||||
|
Table -- hook_fedmsg
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'hook_fedmsg'
|
||||||
|
|
||||||
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
|
project_id = sa.Column(
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey(
|
||||||
|
'projects.id', onupdate='CASCADE', ondelete='CASCADE'),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
index=True)
|
||||||
|
|
||||||
|
active = sa.Column(sa.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
project = relation(
|
||||||
|
'Project', remote_side=[Project.id],
|
||||||
|
backref=backref(
|
||||||
|
'fedmsg_hook', cascade="delete, delete-orphan",
|
||||||
|
single_parent=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FedmsgForm(wtf.Form):
|
||||||
|
''' Form to configure the fedmsg hook. '''
|
||||||
|
active = wtforms.BooleanField(
|
||||||
|
'Active',
|
||||||
|
[wtforms.validators.Optional()]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Fedmsg(BaseHook):
|
||||||
|
''' Fedmsg hooks. '''
|
||||||
|
|
||||||
|
name = 'Fedmsg'
|
||||||
|
description = 'This hook pushes the commit messages'\
|
||||||
|
' to the Fedora bus to be consumed by other applications.'
|
||||||
|
form = FedmsgForm
|
||||||
|
db_object = FedmsgTable
|
||||||
|
backref = 'fedmsg_hook'
|
||||||
|
form_fields = ['active']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def install(cls, project, dbobj):
|
||||||
|
''' Method called to install the hook for a project.
|
||||||
|
|
||||||
|
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||||
|
should be installed
|
||||||
|
|
||||||
|
'''
|
||||||
|
repopaths = [get_repo_path(project)]
|
||||||
|
cls.base_install(repopaths, dbobj, 'fedmsg', 'fedmsg_hook.py')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def remove(cls, project):
|
||||||
|
''' Method called to remove the hook of a project.
|
||||||
|
|
||||||
|
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||||
|
should be installed
|
||||||
|
|
||||||
|
'''
|
||||||
|
repopaths = [get_repo_path(project)]
|
||||||
|
cls.base_remove(repopaths, 'fedmsg')
|
91
pagure/hooks/files/fedmsg_hook.py
Executable file
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import fedmsg
|
||||||
|
import fedmsg.config
|
||||||
|
|
||||||
|
|
||||||
|
if 'PAGURE_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||||
|
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||||
|
|
||||||
|
import pagure
|
||||||
|
import pagure.lib.git
|
||||||
|
|
||||||
|
abspath = os.path.abspath(os.environ['GIT_DIR'])
|
||||||
|
|
||||||
|
|
||||||
|
print "Emitting a message to the fedmsg bus."
|
||||||
|
config = fedmsg.config.load_config([], None)
|
||||||
|
config['active'] = True
|
||||||
|
config['endpoints']['relay_inbound'] = config['relay_inbound']
|
||||||
|
fedmsg.init(name='relay_inbound', **config)
|
||||||
|
|
||||||
|
|
||||||
|
seen = []
|
||||||
|
|
||||||
|
# Read in all the rev information git-receive-pack hands us.
|
||||||
|
for line in sys.stdin.readlines():
|
||||||
|
(oldrev, newrev, refname) = line.strip().split(' ', 2)
|
||||||
|
|
||||||
|
forced = False
|
||||||
|
if set(newrev) == set(['0']):
|
||||||
|
print "Deleting a reference/branch, so we won't run the "\
|
||||||
|
"pagure hook"
|
||||||
|
break
|
||||||
|
elif set(oldrev) == set(['0']):
|
||||||
|
print "New reference/branch"
|
||||||
|
oldrev = '^%s' % oldrev
|
||||||
|
elif pagure.lib.git.is_forced_push(oldrev, newrev, abspath):
|
||||||
|
forced = True
|
||||||
|
base = pagure.lib.git.get_base_revision(oldrev, newrev, abspath)
|
||||||
|
if base:
|
||||||
|
oldrev = base[0]
|
||||||
|
|
||||||
|
revs = pagure.lib.git.get_revs_between(
|
||||||
|
oldrev, newrev, abspath, refname, forced=forced)
|
||||||
|
project_name = pagure.lib.git.get_repo_name(abspath)
|
||||||
|
username = pagure.lib.git.get_username(abspath)
|
||||||
|
project = pagure.lib.get_project(pagure.SESSION, project_name, username)
|
||||||
|
if not project:
|
||||||
|
project = project_name
|
||||||
|
|
||||||
|
auths = set()
|
||||||
|
for rev in revs:
|
||||||
|
email = pagure.lib.git.get_author_email(rev, abspath)
|
||||||
|
name = pagure.lib.git.get_author(rev, abspath)
|
||||||
|
author = pagure.lib.search_user(pagure.SESSION, email=email) or name
|
||||||
|
auths.add(author)
|
||||||
|
|
||||||
|
authors = []
|
||||||
|
for author in auths:
|
||||||
|
if isinstance(author, basestring):
|
||||||
|
author = author
|
||||||
|
else:
|
||||||
|
author = author.to_json(public=True)
|
||||||
|
authors.append(author)
|
||||||
|
|
||||||
|
if revs:
|
||||||
|
revs.reverse()
|
||||||
|
print "* Publishing information for %i commits" % len(revs)
|
||||||
|
pagure.lib.notify.log(
|
||||||
|
project=project,
|
||||||
|
topic="git.receive",
|
||||||
|
msg=dict(
|
||||||
|
total_commits=len(revs),
|
||||||
|
start_commit=revs[0],
|
||||||
|
end_commit=revs[-1],
|
||||||
|
branch=refname,
|
||||||
|
forced=forced,
|
||||||
|
authors=list(authors),
|
||||||
|
agent=os.environ['GL_USER'],
|
||||||
|
repo=project.to_json(public=True)
|
||||||
|
if not isinstance(project, basestring) else project,
|
||||||
|
),
|
||||||
|
)
|