aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNicolas Vigier <boklm@mageia.org>2011-01-04 16:15:53 +0000
committerNicolas Vigier <boklm@mageia.org>2011-01-04 16:15:53 +0000
commit88a840788e82289d417983acf4b49f2c2778296d (patch)
tree2a73dbdb181d05fc5417dbc75baf6fa0a4c73ccb
downloadmgarepo-88a840788e82289d417983acf4b49f2c2778296d.tar
mgarepo-88a840788e82289d417983acf4b49f2c2778296d.tar.gz
mgarepo-88a840788e82289d417983acf4b49f2c2778296d.tar.bz2
mgarepo-88a840788e82289d417983acf4b49f2c2778296d.tar.xz
mgarepo-88a840788e82289d417983acf4b49f2c2778296d.zip
fix problem with python threads on 2010.1
-rw-r--r--BRANCH419
-rw-r--r--CHANGES166
-rw-r--r--MANIFEST.in10
-rw-r--r--README12
-rw-r--r--README.LDAP61
-rw-r--r--RepSys/ConfigParser.py434
-rw-r--r--RepSys/__init__.py19
-rw-r--r--RepSys/binrepo.py394
-rw-r--r--RepSys/cgi/__init__.py0
-rw-r--r--RepSys/cgi/soapserver.py93
-rw-r--r--RepSys/cgi/submit.py119
-rw-r--r--RepSys/cgi/xmlrpcserver.py111
-rw-r--r--RepSys/cgiutil.py53
-rw-r--r--RepSys/command.py61
-rw-r--r--RepSys/commands/__init__.py0
-rw-r--r--RepSys/commands/authoremail.py37
-rw-r--r--RepSys/commands/changed.py41
-rw-r--r--RepSys/commands/ci.py35
-rw-r--r--RepSys/commands/co.py67
-rw-r--r--RepSys/commands/create.py34
-rw-r--r--RepSys/commands/del.py30
-rw-r--r--RepSys/commands/editlog.py39
-rw-r--r--RepSys/commands/getspec.py38
-rw-r--r--RepSys/commands/getsrpm.py100
-rw-r--r--RepSys/commands/log.py62
-rw-r--r--RepSys/commands/markrelease.py103
-rw-r--r--RepSys/commands/patchspec.py38
-rw-r--r--RepSys/commands/putsrpm.py59
-rw-r--r--RepSys/commands/rpmlog.py68
-rw-r--r--RepSys/commands/submit.py211
-rw-r--r--RepSys/commands/switch.py33
-rw-r--r--RepSys/commands/sync.py38
-rw-r--r--RepSys/commands/up.py22
-rw-r--r--RepSys/commands/upload.py28
-rw-r--r--RepSys/layout.py207
-rw-r--r--RepSys/log.py633
-rw-r--r--RepSys/mirror.py129
-rw-r--r--RepSys/plugins/__init__.py27
-rw-r--r--RepSys/plugins/ldapusers.py189
-rw-r--r--RepSys/plugins/sample.py.txt14
-rw-r--r--RepSys/rpmutil.py759
-rw-r--r--RepSys/simplerpm.py19
-rw-r--r--RepSys/svn.py430
-rw-r--r--RepSys/util.py141
-rwxr-xr-xcreate-srpm126
-rw-r--r--default.chlog41
-rwxr-xr-xrepsys93
-rw-r--r--repsys-example.conf74
-rwxr-xr-xrepsys-ssh2
-rw-r--r--repsys.8218
-rw-r--r--repsys.conf18
-rw-r--r--revno.chlog41
-rw-r--r--setup.cfg3
-rwxr-xr-xsetup.py33
54 files changed, 6232 insertions, 0 deletions
diff --git a/BRANCH b/BRANCH
new file mode 100644
index 0000000..ebb87c7
--- /dev/null
+++ b/BRANCH
@@ -0,0 +1,419 @@
+================================
+The detached binaries repository
+================================
+
+.. contents::
+
+A brief description
+===================
+
+Ideally, all binaries from packages sources (ie. all the binary files inside
+SOURCES/) will be placed in another subversion repository. This repository
+is called "tarballs repository", "binaries repository" or just "binrepo".
+It will contain mostly the same directory structure of the main repository,
+but instead of having SOURCES and SPECS, it will only have a SOURCES
+directory. Every copy/move operation should happen in both repositories.
+
+In order to allow deceasing binaries from older distributions, each stable
+distro will have its own subversion repository for binary files. repsys
+knows how to access these binrepos by checking which URL defined in the
+"[binrepo]" section of the configuration file matches the path-part of the
+repository being accessed. (see open issues)
+
+The package changelogs will be generated from SVN commit logs in the main
+"plaintext" repository ("txtrepo" for short) only. Old changelogs will be
+preserved, as even empty revisions are preserved in the binaries-filtering
+conversion.
+
+
+Mapping repositories states
+---------------------------
+
+In order to allow the use of `repsys {getsrpm,co} -r REV`, repsys will have
+to use a reference in the text repo which will be used to know in what
+state was the binrepo when a binary was uploaded.
+
+We cannot use direct revision number mapping through properties/files/etc
+mainly because we may have multiple binaries repositories, and eventually
+they can be filtered for reducing space, thus can't ensure revisions will
+survive. Thus another mechanism which relies on dates instead of revisions
+numbers is needed.
+
+When a binary is uploaded to the binrepo, the file `sha1.lst` is updated to
+have the files's hash and commited in the main text repo. This file will be
+used as the reference when the user uses -r REV on repsys. repsys will
+checkout the package in the main text repo with -r REV and then will use
+the "Last Changed Date" of `sha1.lst` to checkout the binrepo part. Thus,
+`sha1.lst` should be always commited to the main text repository *after* the
+corresponding binary files have been commited to the binrepo. Hooks in the
+main repository may be used to try to enforce this, by checking if the files
+changed in `sha1.lst` are already commited in the corresponding binrepo.
+
+Computation of `sha1.lst` is unlikely to be an issue:
+
+- it should not happen too often for any given package
+- it takes[0] less than 10s to sha1sum all SOURCES of openoffice.org-3.1-1mdv2010.0.src.rpm
+- it probably takes way less than the time to upload the file into the repository
+- it can be computed in parallel to the binrepo commit, and probably finish
+ before that, thus ready by the time `sha1.lst` should be commited
+- users don't need to verify the SHA1s "everytime", but the build system
+ does, thus Repsys can default to not verify and avoid wasting users' time
+
+The use of `sha1.lst` has the valuable property of tying the state of the main
+repository and the binrepo. With it, at getsrpm time of a package
+submission we can verify the SHA1 of the SOURCES-bin, and be sure that
+either the package will be built with the expected state, or early fail the
+build. It also allows for verifying binaries without trusting the binrepo,
+which may be useful if we consider using an unversioned plain filesystem
+storage in the future (for old distros or whatever), or at "client side",
+which maintainers may find useful.
+
+[0]: In a single core AMD Athlon(tm) 3800+ (2400Mhz)
+
+Mapping of revisions using SVN properties
+-----------------------------------------
+
+Alternatively to using the above "sha1.lst scheme", the revision mapping
+between the main repository and a binrepo could be done using subversion
+properties. This could be done by making every commit to binrepos also
+cause a corresponding commit in the main text repository to happen, which
+would update a property recording the current date. That is, a subversion
+property in the main text repository would be kept, such that for any given
+main repository revision, the corresponding state of the binrepos is
+obtainable (using the registered date).
+
+This would be "more transparent", as it can be maintened simply by using
+subversion hooks, without user intervention. OTOH, as every time the user
+commits to a binrepo this would result in a commit in the main repository,
+it would require the user to "svn up" the directories from there before
+commiting, after every binrepo commit. Also, this might result in a big
+number of "bogus" commits to the main repository, which could be seen as log
+pollution, and may potentially increase space usage etc..
+
+Why a new repository without the tarballs
+==========================================
+
+- the current svn repository is too large, hard to manage
+- big binary files (in general, "tarballs") history is of little value in
+ the distro development, we care much more about our specs, patches,
+ configurations, etc.; nonetheless, those big files we don't care much for
+ take the most resources and make backups and restoration in case of
+ failure very expensive, much more so than the more valuable data
+- there is no easy way to strip undesired tarballs without recreating the
+ whole repository
+- fedora and ubuntu have separated repositories, so we must have it too!
+
+Numbers
+-------
+
+Current repository is +390000 revisions and ~340Gb big, while the bzip2ed
+dumps backup for it takes about a bit more than half that size (FIXME:
+estimative, can't check in the backup server right now). Current txtrepo
+with the same number of revisions is ~180Gb big, takes about 2-3 days to be
+imported, while the gzipped full dump backup for it currently takes ~1.2Gb.
+Initial binrepo for Cooker (only `current/` packages' branches) took ~28Gb
+in disk, gzipped full dump for it takes ~25Gb, took about 5h30m to be
+populated from the current in use repository ("oldrepo").
+
+
+Drawbacks of this layout
+=========================
+
+- (always) everything that changes the single-repository usage increases the chance
+ of failure and make things more complicated.
+- subversion can't be used alone as easily as the current scheme allows
+- copying binaries between distro branches may not be "svn-cheap" anymore
+ (unless they're in the same binrepo)
+- ...
+
+
+Open issues
+============
+
+Multiple binrepos dont allow us to have one permanent URL
+---------------------------------------------------------
+
+We would have to update the configuration files from all the users in order
+to add a new stable repository. spuk suggests to use properties in the main
+text repo that would point to the right repository locations.
+
+How to handle failures when operating on more repositores?
+----------------------------------------------------------
+
+binrepos should replicate the structure of the main text repo. What we
+should do if the markrelease succeeds in the binrepo, but fails in the main
+text repo?
+
+R: Markrelease must be done first in the txtrepo. If it fails there "we're
+in trouble" (though currently, we just miss it[0]). When the markrelease is
+done in the txtrepo, we can do markrelease in the binrepo using '-r {DATE}',
+using the markrelease date in the txtrepo as '{DATE}'.
+
+[0] We should add transaction support for markrelease. The transaction could
+be stored out of the packages SVN (another SVN, a DB, a txt file, etc.), and
+would work like:
+
+0. mark beginning of markrelease, early failing the package build if it fails
+1. do markrelease
+2. mark sucessful end of markrelease
+ or mark failed markrelease, so we can replay it later
+
+
+Interesting use cases (first phase)
+===================================
+
+repsys co 2008.1/mutt
+---------------------
+
+- repsys checkouts
+ http://svn.mandriva.com/svn/packages/updates/2008.1/mutt/current to the
+ mutt directory
+
+- repsys checkouts
+ http://svn.mandriva.com/svn/binrepo/updates/2008.1/mutt/current/SOURCES
+ into mutt/SOURCES-bin
+
+- creates symlinks for all files found in SOURCES-bin/ into ../SOURCES/
+
+ (rpm doesn't handle symlinks, this allows us to have explicit links and
+ proper src.rpm generates by rpmbuild)
+
+In case the path doesn't exist in the binrepo it will not fail, as we may
+have not imported all packages or the repository is not prepared to work on
+this model, etc.
+
+markrelease of a package
+------------------------
+
+::
+
+ $ repsys markrelease
+
+- will copy current/ to releases/VERSION/RELEASE, as usual
+
+- will copy current/ to releases/, on the binrepo too
+
+Optionally, markrelease could create revprops indicating which is the
+revision of current/ on the binrepo that represents the tarballs that are
+being tagged.
+
+
+Use cases to be implemented after the first phase
+=================================================
+
+upgrading to a newer version of the package
+-------------------------------------------
+
+::
+
+ $ cd bla/SOURCES/
+ $ wget http://prdownloads.sourceforge.net/bla/bla-1.6.tar.bz2
+ $ repsys add bla-1.6.0.tar.bz2
+
+- repsys notices this is a tarball (checking filename and/or file size)
+
+- repsys will move the file to SOURCES-bin/, create the symlink, and svn-add
+ it to the working copy
+
+ $ # the user updates the spec
+
+ $ repsys rm SOURCES/bla-1.5.1.tar.bz2
+
+- it will remove the symlink and run svn rm on
+ SOURCES-bin/bla-1.6.0.tar.bz2::
+
+ $ cd ../ # package top dir
+ $ repsys ci
+
+- repsys will commit the new tarball on SOURCES-bin/ and then on the rest
+ of the working copy
+
+repsys sync would perform these steps too.
+
+importing a package
+-------------------
+
+ $ repsys putsrpm mypkg.src.rpm
+
+- repsys will open the src.rpm
+
+- will look for tarballs inside SOURCES/ and import them to
+ http://svn.mandriva.com/svn/binrepo/cooker/mypkg/current/SOURCES/
+
+- will move the tarballs out of SOURCES and import the remaining files to
+ http://svn.mandriva.com/svn/packages/cooker/mypkg/current/
+
+- will do whatever else putsrpm already does
+
+TODO
+=====
+
+First phase
+-----------
+
+- upload
+- markrelease
+- putsrpm
+- getsrpm
+
+
+Second phase
+------------
+
+- up
+- sync
+
+Rejected or postponed ideas
+===========================
+
+Use of a plain filesystem storage for the tarballs
+--------------------------------------------------
+
+This was planned, then rejected. It becomes too complicated when thinking
+about markrelease, and mapping SVN revisions in the main repository to
+binaries versions in the "tarballs storage", basically requiring
+implementing VCS-like features on top of filesystem. Would also require
+implementing another authentication and access scheme. The main feature
+would be ease of removing old binaries, which isn't much of a point because
+we don't know precisely what and when we want to remove, so may end up not
+removing much files anyway.
+
+Use of a plain unversioned filesystem storage for the tarballs
+--------------------------------------------------------------
+
+Different than the previous one, this would mean not relying at all on
+binary files history keeping. Structure could be something simple like::
+
+ packages/${pkg:0:1}/$pkg/$tarball
+
+This alternative does not suffice for Cooker, nor for supported distros, for
+which we want history. It could, however, at some point be used for "very
+old" distros, for which we may have lost interest in keeping *binaries*
+history (package history will kept "forever" in the main SVN repository).
+Alternatively, "resetting" an SVN binrepo (i.e. recreate the repository) to
+contain only the latest tarballs would probably take about the same amount
+of space, anyway...
+
+Open tarballs repository
+------------------------
+
+This idea is not really rejected. It does not go against splitting txtrepo
+and binrepo, but rather complement this idea, where the
+open-tarballs-repository would take the place of the binrepo. The txtrepo
+would still be used +- the same way. This repository could be used
+selectively, for packages where it makes sense, while most packages could be
+kept "closed", still as tarballs.
+
+Use of externals for more seamless Subversion usage
+---------------------------------------------------
+
+This idea is not discarded, but it just provides easiness. OTOH, it makes
+things more complicated:
+
+- markrelease: externals would have to be updated in order to make it point
+ to the tagged version in the binrepo, otherwise changes in
+ current@binrepo would change older releases;
+- branching whole distro: even though subversion now supports "relative
+ externals", we would have to update the URLs for *every* package on the
+ distro, as the path to reach the binrepo spans the local distribution
+ directory;
+- keeping externals up-to-date (as stated above and below)
+- authentication and access control: only markrelease action done by the
+ build system should be allowed to change externals (so what about importing
+ new packages?)
+- just a convenience, we don't need and shouldn't rely on externals for
+ running the build system, while most people will use the repositories via
+ Repsys, so why spend time to implement and keep it?
+- "svn co" works transparently, cool, but "svn co -r N" does not, otherwise
+ every change in the binrepo would require svn:externals to be updated in
+ the respective package;
+- it does not solve the problem of creating and handling symlinks between
+ SOURCES and SOURCES-bin.
+
+Keeping svn:externals updated for every package has almost the same cost of
+keeping the `sha1.lst` updated, with the difference that in the latter we
+would not have to update every package when creating distro branches.
+
+Use of "external" xdelta to save space on binaries
+--------------------------------------------------
+
+But how? First idea is this could be done by defining a protocol and
+assuming repository manipulation with repsys (for ease). Repsys could
+xdelta tarballs and add it to SVN with a special filename, then use it when
+checking out. Would require a policy/algorithm on when to ditch old whole
+binaries, too (i.e. hopefully wouldn't need to be handled manually by the
+maintainer). Also, this is something complemental to splitting the
+repository, so we may do it later, for binrepos.
+
+
+The Future
+==========
+
+- Open tarballs repositories
+
+ - suited for GIT, maybe multi-VCS
+ - incremental move
+ - not everything will be suited for this, must handle all cases or be
+ optional
+
+- Xdelta
+
+
+Deployment
+==========
+
+The current repository will be kept around for a while, in readonly state.
+Initial binrepos will be populated with the binaries in the `current/`
+branches of packages.
+
+The binrepo mappings config might be kept in a fixed subversion revision
+property (revision 0?).
+
+Rough steps
+-----------
+
+- check for agreement between subversion repository filters for binaries,
+ and repsys
+- upgrade repsys everywhere
+
+ - kenobi
+ - cluster nodes
+ - raoh
+ - titan
+
+- populate the binrepos for each supported distro, from a specific revision
+ of oldrepo, and mass commmit the corresponding `sha1.lst` in txtrepo for
+ every package
+
+ - set svn:date revprop of the `sha1.lst` mass commit to the date of the
+ oldrepo revision
+ - before mass commiting the `sha1.lst`, possibly freeze oldrepo, check
+ for changes to sources after the selected revision, and update the
+ binrepo as necessary
+
+- check Secteam scripts, make needed changes to get them ready (non
+ critical)
+- set up the new repositories
+
+ - hook for filtering of disallowed (binary) files in main repository
+ - binrepos mappings
+
+- make the new main + binrepos repositories available, but readonly
+
+ - keep new main repository in sync with the old repository with hooks
+
+- make current repository readonly and enable verification of sha1.lst at
+ package submission time
+
+- make sure new main repository and old repository are in sync
+
+ - resync binrepos with the old repository as needed
+
+- final tests
+
+ - change something
+ - submit
+ - etc.
+
+- make the new repositories writeable
+
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..17efc29
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,166 @@
+* 1.9.1-binrepo
+- fix problem with python threads on 2010.1
+
+* 1.9-binrepo
+- added support to "binary repository"
+- added new commands upload, up and del to help handling tarballs in the
+ binaries repository
+
+* 1.9
+- really fixed -M
+- new command log: shows the svn log for a given package
+- added option -s to co, to allow checking out only SPECS
+- use a better message when checking out from the mirror
+- show the path where getspec wrote the spec file
+
+* 1.8
+- make the -M option work again
+- sync now adds the spec file (pointed by blino)
+- fixed putsrpm to not create old log files for packages without changelog
+- submit now can fetch the revision number if not specified (it also shows
+ the author and the first line of the commit)
+- allow submitting many packages at once, even without support on server
+ side (the same behavior as running repsys submit for each package)
+- append a parameter sid=UUID for every set of packages submitted
+- strip username from package URL on submit (#53939)
+- clearer error message when svn co fails
+- svn authentication errors are handled, pointing to the wiki page on
+ configuration
+- svn commands will not be shown on error messages, unless using --debug
+- repsys uses the wrapper repsys-ssh for svn, to handle signals and also
+ set BatchMode
+- added the configuration option svn-env
+- show the error from rpm when fetching the version from a spec fails
+
+* 1.7
+- dropped all authenticated access support: subversion authentication has
+ been broken for a long time and the workarounds weren't decent. It will
+ be back in 1.7.x.
+- added configuration option svn-command in the global section, allowing
+ to replace the default svn command
+- force svn+ssh:// URLs to be in BatchMode, in order to not have any
+ interactivity at all with ssh
+- fixed incompatibility with Python-2.4 on urlparse
+- fixed emptylog message, which was not being shown when needed
+- distributions can be specified by using <distro>/<package> in all
+ commands
+- allow submitting many packages at once (#28352)
+- package revisions in submit are now specified with name@nnn
+- the user can define groups of packages to be submitted in the section
+ "submit-groups"
+- make 'repsys submit' without package name or revision number work again
+- added option --distro to submit
+- make putsrpm work again
+- added subcommand import as an alias to putsrpm
+- template: hide the first release when it has only invisible lines
+- added initial man page
+- allow resorting changelog entries through the config option sort in the
+ log section
+- added rpmlog options: -o to append the old changelog, -p to append the
+ changelog found in the spec, and -s to resort all changelog entries
+- rpmlog, getsrpm, getspec and changed will use the mirror if enabled
+- don't hide authors with only the first revision SILENTed (#41117)
+- fixed bad url used when using -v in getsrpm
+- if REPSYS_CONF is set, /etc/repsys.conf and ~/.repsys/config will not be
+ readed anymore
+- sort the final changelog by enabling the option sort in the log section
+- merge the changelog found in the spec by enabling the option merge-spec
+ in the log section
+- changed the built-in template to the current default.chlog
+- added option -d to repsys sync, to download the missing source files
+- added option -F to repsys ci to set a log message file
+- added option --strict to getsrpm to check if the revision provided
+ matches the package URL;
+- changed the default command to build SRPMs to rpmbuild
+- added configuration option rpmbuild to the section helper, to define the
+ command used to build packages
+- added boolean configuration option strict-revision in the submit
+ section, to allow forcing the use of --strict
+- added option --list in create-srpm to list the available targets
+- make submit -l work on svn+ssh:// targets
+- the fix for the unreleased commits problem in the previous release was
+ wrong, really fixed it
+- moved all configuration options that will hardly be changed to
+ repsys-example.conf; we now have a shorter repsys.conf
+- fixed the use of file:/// URLs when using just the package name
+- allow using submit with package URLs having usernames
+- don't give the wrong message "invalid command 'CMD'" when this is not
+ the case
+- added more help messages in subcommands
+
+* 1.6.19
+- added complement for SILENT: CLOG, which hides everything that does not
+ start with this token
+- fixed generation of unreleased commits, it was using the previous
+ markrelease revision as reference
+- added option -o to 'co' to disable the use of mirror when checking out
+
+* 1.6.18
+- added the subcommand "switch" to help with mirrors support
+- initialize plugins in create-srpm too
+- changelog: perform less svn calls to obtain release number and oldlog
+- changelog: show epoch even in the entry not released
+- changelog: make default.chlog compatible with cheetah-2
+- make "sync" compatible with rpm-4.4.8 behavior
+- "co" don't use mirror when URL is provided
+- "ci" don't relocate back to mirrors after commit (should use switch)
+- ldapusers: added options ldap-uri and ldap-starttls
+- fixed use of __import__, incompatible with python2.4 in plugin support
+- fixed bug of wrong paths when using mirrors
+
+* 1.6.17
+- brought from mdvsys world the sync command
+- ldapusers: the configuration format has changed, now it uses python
+ template strings
+- ldapusers: many fixes: better error messages, ldap-port working, results
+ contain only the fields needed, unbinding after search, filters are
+ escaped
+
+* 1.6.16
+- introduced the plugin ldapusers: repsys user data obtained from LDAP;
+ this plugin is builtin
+- added support to plugins, and the hability to wrap configuration sections
+- added workaround in the template to ignore empty releases
+- added initial support to mirrors, as requested by mrl; it required the
+ new subcommand "ci"
+- changelogs from misc/ will come from HEAD and should be escaped (%%)
+
+* 1.6.15
+- empty changelog entries are now shown, with a EMPTYLOG tag to allow
+ rpmlint warn the developer about it
+- check (and warn) if a temporary package has already been removed before
+ trying to remove it
+
+* 1.6.2b
+- make submit pass --define options to create-srpm script
+- print error message when create-srpm fails
+- make get_srpm return the srpms list
+- add upload-srpm support in create-srpm
+
+* 1.6.2a
+- moved revision-offset to [log] section and added a comment
+
+* 1.6.2
+- reimplemented the option -n for rpmlog, which now uses the svn option
+ --limit.
+- added the option revision-offset, for the Zero Day Revision issue.
+- small fix in main repsys help message
+
+* 1.6.1
+- added option url-map, as an workaround for svn+ssh:// urls problems
+- added configuration sectiom "helper"
+- added getsrpm-mdk and create-srpm to setup.py
+- added option -r to submit
+
+* 1.6.0
+- improved markrelease command line parsing
+- changelogs entries are now groupped by author, and sorted by revision
+ number
+- the changelog now is generated using the Cheetah Template Engine, to
+ allow quick modifications without spending time reading code and
+ introducing new bugs
+- consequently, was added an option "-T <file>" to rpmlog and getsrpm to
+ allow choosing the path of the template to be used
+- added options noauth=0, and baseurl=None in order to disable the
+ authentication in some url schemes (http:// and file://)
+- replaced some "cl" references to "mdv"
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..acf8138
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,10 @@
+recursive-include RepSys *.py
+include RepSys/plugins/*.txt
+include repsys repsys.conf MANIFEST.in
+include repsys.8
+include README
+include CHANGES
+include README.LDAP
+include repsys-example.conf
+include *.chlog
+include create-srpm repsys-ssh
diff --git a/README b/README
new file mode 100644
index 0000000..74aea90
--- /dev/null
+++ b/README
@@ -0,0 +1,12 @@
+repsys is the tool used to manage RPM packages in a subversion repository.
+It is used to create, tag releases, generate .src.rpm, generate changelog,
+and request new package releases for build. It mostly acts as a interface
+to svn(1) commands and small task scripts run that on the build system side
+over ssh(1).
+
+For more information, see repsys(8) and
+http://wiki.mandriva.com/en/Development/Packaging/RepositorySystem
+
+The discussion on the development of repsys takes place on the
+"maintainers" mailing list:
+http://wiki.mandriva.com/en/Development/Mailinglists
diff --git a/README.LDAP b/README.LDAP
new file mode 100644
index 0000000..c22a7fa
--- /dev/null
+++ b/README.LDAP
@@ -0,0 +1,61 @@
+A Repsys plugin for obtaining users from a LDAP server.
+
+In order to enable the plugin, the user must define the following
+options in the [global] section of repsys.conf:
+
+ ldap-uri [required if ldap-server is unset]
+ the URI of the server, you can refer to more than one server by
+ adding more URIs separated by spaces::
+
+ ldap-uri = ldap://ldap.network/ ldaps://backup.network:22389/
+
+ ldap-server [required if ldap-uri is unset]
+ the host name of the LDAP server
+ ldap-port [optional] [default: 389]
+ the port of the LDAP server
+ ldap-base [required]
+ the base DN where the search will be performed
+ ldap-binddn [optional] [default: empty]
+ the DN used to bind
+ ldap-bindpw [optional] [default: empty]
+ the password used to bind
+ ldap-starttls [optional] [default: no]
+ use "yes" or "no" to enable or disable the use of the STARTTLS
+ LDAP extension
+ ldap-filterformat [optional]
+ [default: (&(objectClass=inetOrgPerson)(uid=$username))]
+ RFC-2254 filter string used in the search of the user entry.
+ Note that this is a python template string and will have the
+ user name as parameter. For example:
+
+ ldap-filterformat = (&(objectClass=inetOrgPerson)(uid=$username))
+
+ Will result in the search filter:
+
+ (&(objectClass=inetOrgPerson)(uid=john))
+
+ ldap-resultformat [optional] [default: $cn <$mail>]
+ This is a python template string. This string will be
+ formatted using one dict object containing the fields
+ returned in the LDAP search, for example:
+
+ >>> format = Template("$cn <$mail>")
+ >>> d = search(basedn, filter)
+ >>> d
+ {"cn": "John Doe", "mail": "john@mandriva.org",
+ "uidNumber": "1290", "loginShell": "/bin/bash",
+ ... many other attributes ... }
+ >>> value = format.substitute(d)
+ >>> print value
+ John Doe <john@mandriva.org>
+
+ Note that only the first value of the attributes will be
+ used.
+
+When the searched option is not found, it will try in repsys.conf. All
+the values found. (including from repsys.conf) will be cached between
+each configuration access.
+
+This plugin requires the package python-ldap.
+
+For more information, look http://qa.mandriva.com/show_bug.cgi?id=30549
diff --git a/RepSys/ConfigParser.py b/RepSys/ConfigParser.py
new file mode 100644
index 0000000..3b4e213
--- /dev/null
+++ b/RepSys/ConfigParser.py
@@ -0,0 +1,434 @@
+"""
+This is a heavily hacked version of ConfigParser to keep the order in
+which options and sections are read, and allow multiple options with
+the same key.
+"""
+from __future__ import generators
+import string, types
+import re
+
+__all__ = ["NoSectionError","DuplicateSectionError","NoOptionError",
+ "InterpolationError","InterpolationDepthError","ParsingError",
+ "MissingSectionHeaderError","ConfigParser",
+ "MAX_INTERPOLATION_DEPTH"]
+
+DEFAULTSECT = "DEFAULT"
+
+MAX_INTERPOLATION_DEPTH = 10
+
+# exception classes
+class Error(Exception):
+ def __init__(self, msg=''):
+ self._msg = msg
+ Exception.__init__(self, msg)
+ def __repr__(self):
+ return self._msg
+ __str__ = __repr__
+
+class NoSectionError(Error):
+ def __init__(self, section):
+ Error.__init__(self, 'No section: %s' % section)
+ self.section = section
+
+class DuplicateSectionError(Error):
+ def __init__(self, section):
+ Error.__init__(self, "Section %s already exists" % section)
+ self.section = section
+
+class NoOptionError(Error):
+ def __init__(self, option, section):
+ Error.__init__(self, "No option `%s' in section: %s" %
+ (option, section))
+ self.option = option
+ self.section = section
+
+class InterpolationError(Error):
+ def __init__(self, reference, option, section, rawval):
+ Error.__init__(self,
+ "Bad value substitution:\n"
+ "\tsection: [%s]\n"
+ "\toption : %s\n"
+ "\tkey : %s\n"
+ "\trawval : %s\n"
+ % (section, option, reference, rawval))
+ self.reference = reference
+ self.option = option
+ self.section = section
+
+class InterpolationDepthError(Error):
+ def __init__(self, option, section, rawval):
+ Error.__init__(self,
+ "Value interpolation too deeply recursive:\n"
+ "\tsection: [%s]\n"
+ "\toption : %s\n"
+ "\trawval : %s\n"
+ % (section, option, rawval))
+ self.option = option
+ self.section = section
+
+class ParsingError(Error):
+ def __init__(self, filename):
+ Error.__init__(self, 'File contains parsing errors: %s' % filename)
+ self.filename = filename
+ self.errors = []
+
+ def append(self, lineno, line):
+ self.errors.append((lineno, line))
+ self._msg = self._msg + '\n\t[line %2d]: %s' % (lineno, line)
+
+class MissingSectionHeaderError(ParsingError):
+ def __init__(self, filename, lineno, line):
+ Error.__init__(
+ self,
+ 'File contains no section headers.\nfile: %s, line: %d\n%s' %
+ (filename, lineno, line))
+ self.filename = filename
+ self.lineno = lineno
+ self.line = line
+
+class ConfigParser:
+ def __init__(self, defaults=None):
+ # Options are stored in __sections_list like this:
+ # [(sectname, [(optname, optval), ...]), ...]
+ self.__sections_list = []
+ self.__sections_dict = {}
+ if defaults is None:
+ self.__defaults = {}
+ else:
+ self.__defaults = defaults
+
+ def defaults(self):
+ return self.__defaults
+
+ def sections(self):
+ return self.__sections_dict.keys()
+
+ def has_section(self, section):
+ return self.__sections_dict.has_key(section)
+
+ def options(self, section):
+ self.__sections_dict[section]
+ try:
+ opts = self.__sections_dict[section].keys()
+ except KeyError:
+ raise NoSectionError(section)
+ return self.__defaults.keys()+opts
+
+ def read(self, filenames):
+ if type(filenames) in types.StringTypes:
+ filenames = [filenames]
+ for filename in filenames:
+ try:
+ fp = open(filename)
+ except IOError:
+ continue
+ self.__read(fp, filename)
+ fp.close()
+
+ def readfp(self, fp, filename=None):
+ if filename is None:
+ try:
+ filename = fp.name
+ except AttributeError:
+ filename = '<???>'
+ self.__read(fp, filename)
+
+ def set(self, section, option, value):
+ if self.__sections_dict.has_key(section):
+ sectdict = self.__sections_dict[section]
+ sectlist = []
+ self.__sections_list.append((section, sectlist))
+ elif section == DEFAULTSECT:
+ sectdict = self.__defaults
+ sectlist = None
+ else:
+ sectdict = {}
+ self.__sections_dict[section] = sectdict
+ sectlist = []
+ self.__sections_list.append((section, sectlist))
+ xform = self.optionxform(option)
+ sectdict[xform] = value
+ if sectlist is not None:
+ sectlist.append([xform, value])
+
+ def get(self, section, option, raw=0, vars=None):
+ d = self.__defaults.copy()
+ try:
+ d.update(self.__sections_dict[section])
+ except KeyError:
+ if section != DEFAULTSECT:
+ raise NoSectionError(section)
+ if vars:
+ d.update(vars)
+ option = self.optionxform(option)
+ try:
+ rawval = d[option]
+ except KeyError:
+ raise NoOptionError(option, section)
+ if raw:
+ return rawval
+ return self.__interpolate(rawval, d)
+
+ def getall(self, section, option, raw=0, vars=None):
+ option = self.optionxform(option)
+ values = []
+ d = self.__defaults.copy()
+ if section != DEFAULTSECT:
+ for sectname, options in self.__sections_list:
+ if sectname == section:
+ for optname, value in options:
+ if optname == option:
+ values.append(value)
+ d[optname] = value
+ if raw:
+ return values
+ if vars:
+ d.update(vars)
+ for i in len(values):
+ values[i] = self.__interpolate(values[i], d)
+ return values
+
+ def walk(self, section, option=None, raw=0, vars=None):
+ # Build dictionary for interpolation
+ try:
+ d = self.__sections_dict[section].copy()
+ except KeyError:
+ if section == DEFAULTSECT:
+ d = {}
+ else:
+ raise NoSectionError(section)
+ d.update(self.__defaults)
+ if vars:
+ d.update(vars)
+
+ # Start walking
+ if option:
+ option = self.optionxform(option)
+ if section != DEFAULTSECT:
+ for sectname, options in self.__sections_list:
+ if sectname == section:
+ for optname, value in options:
+ if not option or optname == option:
+ if not raw:
+ value = self.__interpolate(value, d)
+ yield (optname, value)
+
+ def __interpolate(self, value, vars):
+ rawval = value
+ depth = 0
+ while depth < 10:
+ depth = depth + 1
+ if value.find("%(") >= 0:
+ try:
+ value = value % vars
+ except KeyError, key:
+ raise InterpolationError(key, option, section, rawval)
+ else:
+ break
+ if value.find("%(") >= 0:
+ raise InterpolationDepthError(option, section, rawval)
+ return value
+
+ def __get(self, section, conv, option):
+ return conv(self.get(section, option))
+
+ def getint(self, section, option):
+ return self.__get(section, string.atoi, option)
+
+ def getfloat(self, section, option):
+ return self.__get(section, string.atof, option)
+
+ def getboolean(self, section, option):
+ states = {'1': 1, 'yes': 1, 'true': 1, 'on': 1,
+ '0': 0, 'no': 0, 'false': 0, 'off': 0}
+ v = self.get(section, option)
+ if not states.has_key(v.lower()):
+ raise ValueError, 'Not a boolean: %s' % v
+ return states[v.lower()]
+
+ def optionxform(self, optionstr):
+ #return optionstr.lower()
+ return optionstr
+
+ def has_option(self, section, option):
+ """Check for the existence of a given option in a given section."""
+ if not section or section == "DEFAULT":
+ return self.__defaults.has_key(option)
+ elif not self.has_section(section):
+ return 0
+ else:
+ option = self.optionxform(option)
+ return self.__sections_dict[section].has_key(option)
+
+ SECTCRE = re.compile(r'\[(?P<header>[^]]+)\]')
+ OPTCRE = re.compile(r'(?P<option>\S+)\s*(?P<vi>[:=])\s*(?P<value>.*)$')
+
+ def __read(self, fp, fpname):
+ cursectdict = None # None, or a dictionary
+ optname = None
+ lineno = 0
+ e = None # None, or an exception
+ while 1:
+ line = fp.readline()
+ if not line:
+ break
+ lineno = lineno + 1
+ # comment or blank line?
+ if line.strip() == '' or line[0] in '#;':
+ continue
+ if line.split()[0].lower() == 'rem' \
+ and line[0] in "rR": # no leading whitespace
+ continue
+ # continuation line?
+ if line[0] in ' \t' and cursectdict is not None and optname:
+ value = line.strip()
+ if value:
+ k = self.optionxform(optname)
+ cursectdict[k] = "%s\n%s" % (cursectdict[k], value)
+ cursectlist[-1][1] = "%s\n%s" % (cursectlist[-1][1], value)
+ # a section header or option header?
+ else:
+ # is it a section header?
+ mo = self.SECTCRE.match(line)
+ if mo:
+ sectname = mo.group('header')
+ if self.__sections_dict.has_key(sectname):
+ cursectdict = self.__sections_dict[sectname]
+ cursectlist = []
+ self.__sections_list.append((sectname, cursectlist))
+ elif sectname == DEFAULTSECT:
+ cursectdict = self.__defaults
+ cursectlist = None
+ else:
+ cursectdict = {}
+ self.__sections_dict[sectname] = cursectdict
+ cursectlist = []
+ self.__sections_list.append((sectname, cursectlist))
+ # So sections can't start with a continuation line
+ optname = None
+ # no section header in the file?
+ elif cursectdict is None:
+ raise MissingSectionHeaderError(fpname, lineno, `line`)
+ # an option line?
+ else:
+ mo = self.OPTCRE.match(line)
+ if mo:
+ optname, vi, optval = mo.group('option', 'vi', 'value')
+ if vi in ('=', ':') and ';' in optval:
+ # ';' is a comment delimiter only if it follows
+ # a spacing character
+ pos = optval.find(';')
+ if pos and optval[pos-1] in string.whitespace:
+ optval = optval[:pos]
+ optval = optval.strip()
+ # allow empty values
+ if optval == '""':
+ optval = ''
+ xform = self.optionxform(optname)
+ cursectdict[xform] = optval
+ if cursectlist is not None:
+ cursectlist.append([xform, optval])
+ else:
+ # a non-fatal parsing error occurred. set up the
+ # exception but keep going. the exception will be
+ # raised at the end of the file and will contain a
+ # list of all bogus lines
+ if not e:
+ e = ParsingError(fpname)
+ e.append(lineno, `line`)
+ # if any parsing errors occurred, raise an exception
+ if e:
+ raise e
+
+# Here we wrap this hacked ConfigParser into something more useful
+# for us.
+
+import os
+
+class Config:
+ def __init__(self):
+ self._config = ConfigParser()
+ self._wrapped = {}
+ conffiles = []
+ repsys_conf = os.environ.get("REPSYS_CONF")
+ if repsys_conf:
+ conffiles.append(repsys_conf)
+ else:
+ conffiles.append("/etc/repsys.conf")
+ conffiles.append(os.path.expanduser("~/.repsys/config"))
+ for file in conffiles:
+ if os.path.isfile(file):
+ self._config.read(file)
+
+ def wrap(self, section, handler, option=None):
+ """Set one wrapper for a given section
+
+ The wrapper must be a function
+ f(section, option=None, default=None, walk=False).
+ """
+ self._wrapped[section] = handler
+
+ def sections(self):
+ try:
+ return self._config.sections()
+ except Error:
+ return []
+
+ def options(self, section):
+ try:
+ return self._config.options(section)
+ except Error:
+ return []
+
+ def set(self, section, option, value):
+ return self._config.set(section, option, value)
+
+ def walk(self, section, option=None, raw=0, vars=None):
+ handler = self._wrapped.get(section)
+ if handler:
+ return handler(section, option, walk=True)
+ return self._config.walk(section, option, raw, vars)
+
+ def get(self, section, option, default=None, raw=False, wrap=True):
+ if wrap:
+ handler = self._wrapped.get(section)
+ if handler:
+ handler = self._wrapped.get(section)
+ return handler(section, option, default)
+ try:
+ return self._config.get(section, option, raw=raw)
+ except Error:
+ return default
+
+ def getint(self, section, option, default=None):
+ ret = self.get(section, option, default)
+ if type(ret) == type(""):
+ return int(ret)
+
+ def getbool(self, section, option, default=None):
+ ret = self.get(section, option, default)
+ states = {'1': 1, 'yes': 1, 'true': 1, 'on': 1,
+ '0': 0, 'no': 0, 'false': 0, 'off': 0}
+ if type(ret) == type("") and states.has_key(ret.lower()):
+ return states[ret.lower()]
+ return default
+
+def test():
+ config = Config()
+ def handler(section, option=None, default=None, walk=False):
+ d = {"fulano": "ciclano",
+ "foolano": "ceeclano"}
+ if walk:
+ return d.items()
+ elif option in d:
+ return d[option]
+ else:
+ return config.get(section, option, default, wrap=False)
+ config.wrap("users", handler=handler)
+ print config.get("users", "fulano") # found in wrapper
+ print config.get("users", "andreas") # found in repsys.conf
+ print config.walk("users")
+
+if __name__ == "__main__":
+ test()
+# vim:ts=4:sw=4:et
diff --git a/RepSys/__init__.py b/RepSys/__init__.py
new file mode 100644
index 0000000..b0df184
--- /dev/null
+++ b/RepSys/__init__.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+import re
+import os
+import tempfile
+
+import ConfigParser
+
+config = ConfigParser.Config()
+tempfile.tempdir = config.get("global", "tempdir", None) or None # when ""
+del ConfigParser
+
+def disable_mirror(*a, **kw):
+ config.set("global", "use-mirror", "no")
+
+class Error(Exception): pass
+
+class SilentError(Error): pass
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/binrepo.py b/RepSys/binrepo.py
new file mode 100644
index 0000000..ad14665
--- /dev/null
+++ b/RepSys/binrepo.py
@@ -0,0 +1,394 @@
+from RepSys import Error, config, mirror, layout
+from RepSys.util import execcmd, rellink
+from RepSys.svn import SVN
+
+import sys
+import os
+import string
+import stat
+import shutil
+import re
+import tempfile
+import hashlib
+import urlparse
+import threading
+from cStringIO import StringIO
+
+DEFAULT_TARBALLS_REPO = "/tarballs"
+BINARIES_DIR_NAME = "SOURCES"
+BINARIES_CHECKOUT_NAME = "SOURCES-bin"
+
+PROP_USES_BINREPO = "mdv:uses-binrepo"
+PROP_BINREPO_REV = "mdv:binrepo-rev"
+
+BINREPOS_SECTION = "binrepos"
+
+SOURCES_FILE = "sha1.lst"
+
+class ChecksumError(Error):
+ pass
+
+def svn_baseurl(target):
+ svn = SVN()
+ info = svn.info2(target)
+ if info is None:
+ # unversioned resource
+ newtarget = os.path.dirname(target)
+ info = svn.info2(newtarget)
+ assert info is not None, "svn_basedir should not be used with a "\
+ "non-versioned directory"
+ root = info["Repository Root"]
+ url = info["URL"]
+ kind = info["Node Kind"]
+ path = url[len(root):]
+ if kind == "directory":
+ return url
+ basepath = os.path.dirname(path)
+ baseurl = mirror.normalize_path(url + "/" + basepath)
+ return baseurl
+
+def svn_root(target):
+ svn = SVN()
+ info = svn.info2(target)
+ if info is None:
+ newtarget = os.path.dirname(target)
+ info = svn.info2(newtarget)
+ assert info is not None
+ return info["Repository Root"]
+
+def enabled(url):
+ #TODO use information from url to find out whether we have a binrepo
+ # available for this url
+ use = config.getbool("global", "use-binaries-repository", False)
+ return use
+
+def default_repo():
+ base = config.get("global", "binaries-repository", None)
+ if base is None:
+ default_parent = config.get("global", "default_parent", None)
+ if default_parent is None:
+ raise Error, "no binaries-repository nor default_parent "\
+ "configured"
+ comps = urlparse.urlparse(default_parent)
+ base = comps[1] + ":" + DEFAULT_TARBALLS_REPO
+ return base
+
+def translate_url(url):
+ url = mirror.normalize_path(url)
+ main = mirror.normalize_path(layout.repository_url())
+ subpath = url[len(main)+1:]
+ # [binrepos]
+ # updates/2009.0 = svn+ssh://svn.mandriva.com/svn/binrepo/20090/
+ ## svn+ssh://svn.mandriva.com/svn/packages/2009.0/trafshow/current
+ ## would translate to
+ ## svn+ssh://svn.mandriva.com/svn/binrepo/20090/updates/trafshow/current/
+ binbase = None
+ if BINREPOS_SECTION in config.sections():
+ for option, value in config.walk(BINREPOS_SECTION):
+ if subpath.startswith(option):
+ binbase = value
+ break
+ binurl = mirror._joinurl(binbase or default_repo(), subpath)
+ return binurl
+
+def translate_topdir(path):
+ """Returns the URL in the binrepo from a given path inside a SVN
+ checkout directory.
+
+ @path: if specified, returns a URL in the binrepo whose path is the
+ same as the path inside the main repository.
+ """
+ baseurl = svn_baseurl(path)
+ binurl = translate_url(baseurl)
+ target = mirror.normalize_path(binurl)
+ return target
+
+def is_binary(path):
+ raw = config.get("binrepo", "upload-match",
+ "\.(7z|Z|bin|bz2|cpio|db|deb|egg|gem|gz|jar|jisp|lzma|"\
+ "pdf|pgn\\.gz|pk3|rpm|rpm|run|sdz|smzip|tar|tbz|"\
+ "tbz2|tgz|ttf|uqm|wad|war|xar|xpi|zip)$")
+ maxsize = config.getint("binrepo", "upload-match-size", "1048576") # 1MiB
+ expr = re.compile(raw)
+ name = os.path.basename(path)
+ if expr.search(name):
+ return True
+ st = os.stat(path)
+ if st[stat.ST_SIZE] >= maxsize:
+ return True
+ return False
+
+def find_binaries(paths):
+ new = []
+ for path in paths:
+ if os.path.isdir(path):
+ for name in os.listdir(path):
+ fpath = os.path.join(path, name)
+ if is_binary(fpath):
+ new.append(fpath)
+ else:
+ if is_binary(path):
+ new.append(path)
+ return new
+
+def make_symlinks(source, dest):
+ todo = []
+ tomove = []
+ for name in os.listdir(source):
+ path = os.path.join(source, name)
+ if not os.path.isdir(path) and not name.startswith("."):
+ destpath = os.path.join(dest, name)
+ linkpath = rellink(path, destpath)
+ if os.path.exists(destpath):
+ if (os.path.islink(destpath) and
+ os.readlink(destpath) == linkpath):
+ continue
+ movepath = destpath + ".repsys-moved"
+ if os.path.exists(movepath):
+ raise Error, "cannot create symlink, %s already "\
+ "exists (%s too)" % (destpath, movepath)
+ tomove.append((destpath, movepath))
+ todo.append((destpath, linkpath))
+ for destpath, movepath in tomove:
+ os.rename(destpath, movepath)
+ for destpath, linkpath in todo:
+ os.symlink(linkpath, destpath)
+
+def download(targetdir, pkgdirurl=None, export=False, show=True,
+ revision=None, symlinks=True, check=False):
+ assert not export or (export and pkgdirurl)
+ svn = SVN()
+ sourcespath = os.path.join(targetdir, "SOURCES")
+ binpath = os.path.join(targetdir, BINARIES_CHECKOUT_NAME)
+ if pkgdirurl:
+ topurl = translate_url(pkgdirurl)
+ else:
+ topurl = translate_topdir(targetdir)
+ binrev = None
+ if revision:
+ if pkgdirurl:
+ binrev = mapped_revision(pkgdirurl, revision)
+ else:
+ binrev = mapped_revision(targetdir, revision, wc=True)
+ binurl = mirror._joinurl(topurl, BINARIES_DIR_NAME)
+ if export:
+ svn.export(binurl, binpath, rev=binrev, show=show)
+ else:
+ svn.checkout(binurl, binpath, rev=binrev, show=show)
+ if symlinks:
+ make_symlinks(binpath, sourcespath)
+ if check:
+ check_sources(targetdir)
+
+def import_binaries(topdir, pkgname):
+ """Import all binaries from a given package checkout
+
+ (with pending svn adds)
+
+ @topdir: the path to the svn checkout
+ """
+ svn = SVN()
+ topurl = translate_topdir(topdir)
+ sourcesdir = os.path.join(topdir, "SOURCES")
+ bintopdir = tempfile.mktemp("repsys")
+ try:
+ svn.checkout(topurl, bintopdir)
+ checkout = True
+ except Error:
+ bintopdir = tempfile.mkdtemp("repsys")
+ checkout = False
+ try:
+ bindir = os.path.join(bintopdir, BINARIES_DIR_NAME)
+ if not os.path.exists(bindir):
+ if checkout:
+ svn.mkdir(bindir)
+ else:
+ os.mkdir(bindir)
+ binaries = find_binaries([sourcesdir])
+ update = update_sources_threaded(topdir, added=binaries)
+ for path in binaries:
+ name = os.path.basename(path)
+ binpath = os.path.join(bindir, name)
+ os.rename(path, binpath)
+ try:
+ svn.remove(path)
+ except Error:
+ # file not tracked
+ svn.revert(path)
+ if checkout:
+ svn.add(binpath)
+ log = "imported binaries for %s" % pkgname
+ if checkout:
+ rev = svn.commit(bindir, log=log)
+ else:
+ rev = svn.import_(bintopdir, topurl, log=log)
+ svn.propset(PROP_USES_BINREPO, "yes", topdir)
+ svn.propset(PROP_BINREPO_REV, str(rev), topdir)
+ update.join()
+ svn.add(sources_path(topdir))
+ finally:
+ shutil.rmtree(bintopdir)
+
+def create_package_dirs(bintopdir):
+ svn = SVN()
+ binurl = mirror._joinurl(bintopdir, BINARIES_DIR_NAME)
+ silent = config.get("log", "ignore-string", "SILENT")
+ message = "%s: created binrepo package structure" % silent
+ svn.mkdir(binurl, log=message, parents=True)
+
+def parse_sources(path):
+ entries = {}
+ f = open(path)
+ for rawline in f:
+ line = rawline.strip()
+ try:
+ sum, name = line.split(None, 1)
+ except ValueError:
+ # failed to unpack, line format error
+ raise Error, "invalid line in sources file: %s" % rawline
+ entries[name] = sum
+ return entries
+
+def check_hash(path, sum):
+ newsum = file_hash(path)
+ if newsum != sum:
+ raise ChecksumError, "different checksums for %s: expected %s, "\
+ "but %s was found" % (path, sum, newsum)
+
+def check_sources(topdir):
+ spath = sources_path(topdir)
+ if not os.path.exists(spath):
+ raise Error, "'%s' was not found" % spath
+ entries = parse_sources(spath)
+ for name, sum in entries.iteritems():
+ fpath = os.path.join(topdir, "SOURCES", name)
+ check_hash(fpath, sum)
+
+def file_hash(path):
+ sum = hashlib.sha1()
+ f = open(path)
+ while True:
+ block = f.read(4096)
+ if not block:
+ break
+ sum.update(block)
+ f.close()
+ return sum.hexdigest()
+
+def sources_path(topdir):
+ path = os.path.join(topdir, "SOURCES", SOURCES_FILE)
+ return path
+
+def update_sources(topdir, added=[], removed=[]):
+ path = sources_path(topdir)
+ entries = {}
+ if os.path.isfile(path):
+ entries = parse_sources(path)
+ f = open(path, "w") # open before calculating hashes
+ for name in removed:
+ entries.pop(removed)
+ for added_path in added:
+ name = os.path.basename(added_path)
+ entries[name] = file_hash(added_path)
+ for name in sorted(entries):
+ f.write("%s %s\n" % (entries[name], name))
+ f.close()
+
+def update_sources_threaded(*args, **kwargs):
+ t = threading.Thread(target=update_sources, args=args, kwargs=kwargs)
+ t.start()
+ t.join()
+ return t
+
+def upload(path, message=None):
+ from RepSys.rpmutil import getpkgtopdir
+ svn = SVN()
+ if not os.path.exists(path):
+ raise Error, "not found: %s" % path
+ # XXX check if the path is under SOURCES/
+ paths = find_binaries([path])
+ if not paths:
+ raise Error, "'%s' does not seem to have any tarballs" % path
+ topdir = getpkgtopdir()
+ bintopdir = translate_topdir(topdir)
+ binurl = mirror._joinurl(bintopdir, BINARIES_DIR_NAME)
+ sourcesdir = os.path.join(topdir, "SOURCES")
+ bindir = os.path.join(topdir, BINARIES_CHECKOUT_NAME)
+ silent = config.get("log", "ignore-string", "SILENT")
+ if not os.path.exists(bindir):
+ try:
+ download(topdir, show=False)
+ except Error:
+ # possibly the package does not exist
+ # (TODO check whether it is really a 'path not found' error)
+ pass
+ if not os.path.exists(bindir):
+ create_package_dirs(bintopdir)
+ svn.propset(PROP_USES_BINREPO, "yes", topdir)
+ svn.commit(topdir, log="%s: created binrepo structure" % silent)
+ download(topdir, show=False)
+ for path in paths:
+ if svn.info2(path):
+ sys.stderr.write("'%s' is already tracked by svn, ignoring\n" %
+ path)
+ continue
+ name = os.path.basename(path)
+ binpath = os.path.join(bindir, name)
+ os.rename(path, binpath)
+ svn.add(binpath)
+ if not message:
+ message = "%s: new binary files %s" % (silent, " ".join(paths))
+ make_symlinks(bindir, sourcesdir)
+ update = update_sources_threaded(topdir, added=paths)
+ rev = svn.commit(binpath, log=message)
+ svn.propset(PROP_BINREPO_REV, str(rev), topdir)
+ sources = sources_path(topdir)
+ svn.add(sources)
+ update.join()
+ svn.commit(topdir + " " + sources, log=message, nonrecursive=True)
+
+def mapped_revision(target, revision, wc=False):
+ """Maps a txtrepo revision to a binrepo datespec
+
+ This datespec can is intended to be used by svn .. -r DATE.
+
+ @target: a working copy path or a URL
+ @revision: if target is a URL, the revision number used when fetching
+ svn info
+ @wc: if True indicates that 'target' must be interpreted as a
+ the path of a svn working copy, otherwise it is handled as a URL
+ """
+ svn = SVN()
+ binrev = None
+ if wc:
+ spath = sources_path(target)
+ if os.path.exists(spath):
+ infolines = svn.info(spath, xml=True)
+ if infolines:
+ rawinfo = "".join(infolines) # arg!
+ found = re.search("<date>(.*?)</date>", rawinfo).groups()
+ date = found[0]
+ else:
+ raise Error, "bogus 'svn info' for '%s'" % spath
+ else:
+ raise Error, "'%s' was not found" % spath
+ else:
+ url = mirror._joinurl(target, sources_path(""))
+ date = svn.propget("svn:date", url, rev=revision, revprop=True)
+ if not date:
+ raise Error, "no valid date available for '%s'" % url
+ binrev = "{%s}" % date
+ return binrev
+
+def markrelease(sourceurl, releasesurl, version, release, revision):
+ svn = SVN()
+ binrev = mapped_revision(sourceurl, revision)
+ binsource = translate_url(sourceurl)
+ binreleases = translate_url(releasesurl)
+ versiondir = mirror._joinurl(binreleases, version)
+ dest = mirror._joinurl(versiondir, release)
+ svn.mkdir(binreleases, noerror=1, log="created directory for releases")
+ svn.mkdir(versiondir, noerror=1, log="created directory for version %s" % version)
+ svn.copy(binsource, dest, rev=binrev,
+ log="%%markrelease ver=%s rel=%s rev=%s binrev=%s" % (version, release,
+ revision, binrev))
diff --git a/RepSys/cgi/__init__.py b/RepSys/cgi/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/RepSys/cgi/__init__.py
diff --git a/RepSys/cgi/soapserver.py b/RepSys/cgi/soapserver.py
new file mode 100644
index 0000000..2f6b751
--- /dev/null
+++ b/RepSys/cgi/soapserver.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys.rpmutil import get_srpm
+from RepSys.cgiutil import CgiError, get_targets
+import sys
+import os
+
+try:
+ import NINZ.dispatch
+except ImportError:
+ NINZ = None
+
+class SoapIface:
+ def author_email(self, author):
+ return config.get("users", author)
+
+ def submit_package(self, packageurl, packagerev, targetname):
+ username = os.environ.get("REMOTE_USER")
+ packager = config.get("users", username)
+ if not packager:
+ raise CgiError, "your email was not found"
+ elif not packagerev:
+ raise CgiError, "no revision provided"
+ elif not targetname:
+ raise CgiError, "no target provided"
+ else:
+ targetname = targetname.lower()
+ for target in get_targets():
+ if target.name.lower() == targetname:
+ break
+ else:
+ raise CgiError, "target not found"
+ try:
+ tmp = int(packagerev)
+ except ValueError:
+ raise CgiError, "invalid revision provided"
+ for allowed in target.allowed:
+ if packageurl.startswith(allowed):
+ break
+ else:
+ raise CgiError, "%s is not allowed for this target" \
+ % packageurl
+ get_srpm(packageurl,
+ revision=packagerev,
+ targetdirs=target.target,
+ packager=packager,
+ revname=1,
+ svnlog=1,
+ scripts=target.scripts)
+ return 1
+
+ def submit_targets(self):
+ return [x.name for x in get_targets()]
+
+TEMPLATE = """\
+Content-type: text/html
+
+<html>
+<head>
+<title>Repository system SOAP server</title>
+</head>
+<body bgcolor="white">
+<br>
+<hr>
+<center>
+<b>%(message)s</b>
+</center>
+<hr>
+</body>
+</html>
+"""
+
+def show(msg="", error=0):
+ if error:
+ msg = '<font color="red">%s</font>' % msg
+ print TEMPLATE % {"message":msg}
+
+def main():
+ if not os.environ.has_key('REQUEST_METHOD'):
+ sys.stderr.write("error: this program is meant to be used as a cgi\n")
+ sys.exit(1)
+ if not NINZ:
+ show("NINZ is not properly installed in this system", error=1)
+ sys.exit(1)
+ username = os.environ.get("REMOTE_USER")
+ method = os.environ.get("REQUEST_METHOD")
+ if not username or method != "POST":
+ show("This is a SOAP interface!", error=1)
+ sys.exit(1)
+
+ NINZ.dispatch.AsCGI(modules=(SoapIface(),))
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/cgi/submit.py b/RepSys/cgi/submit.py
new file mode 100644
index 0000000..10f7cb2
--- /dev/null
+++ b/RepSys/cgi/submit.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys.rpmutil import get_srpm
+from RepSys.cgiutil import CgiError, get_targets
+import cgi
+import sys
+import os
+
+TEMPLATE = """\
+<html>
+<head>
+<title>Repository package submission system</title>
+</head>
+<body bgcolor="white">
+<table cellspacing=0 cellpadding=0 border=0 width="100%%">
+ <tr bgcolor="#020264"><td align="left" valign="middle"><img src="http://qa.mandriva.com/mandriva.png" hspace=0 border=0 alt=""></td></tr>
+</table>
+<br>
+<hr>
+<center>
+<b>%(message)s</b>
+<br><br>
+<form method="POST" action="">
+<table><tr><td valign="top">
+ Package URL:<br>
+ <input name="packageurl" size="60" value="svn+ssh://cvs.mandriva.com/svn/mdv/cooker/"><br>
+ <small>Ex. svn+ssh://cvs.mandriva.com/svn/mdv/cooker/pkgname</small><br>
+ </td><td valign="top">
+ Revision:<br>
+ <input name="packagerev" size="10" value=""><br>
+ </td></tr></table>
+ <br>
+ Package target:<br>
+ <select name="target" size=5>
+ %(targetoptions)s
+ </select><br>
+ <br>
+ <input type="submit" value="Submit package">
+</form>
+</center>
+<hr/>
+</body>
+</html>
+"""
+
+def get_targetoptions():
+ s = ""
+ selected = " selected"
+ for target in get_targets():
+ s += '<option value="%s"%s>%s</option>' \
+ % (target.name, selected, target.name)
+ selected = ""
+ return s
+
+def show(msg="", error=0):
+ if error:
+ msg = '<font color="red">%s</font>' % msg
+ print TEMPLATE % {"message":msg, "targetoptions":get_targetoptions()}
+
+def submit_packages(packager):
+ form = cgi.FieldStorage()
+ packageurl = form.getfirst("packageurl", "").strip()
+ packagerev = form.getfirst("packagerev", "").strip()
+ if not packageurl:
+ show()
+ elif not packagerev:
+ raise CgiError, "No revision provided!"
+ else:
+ targetname = form.getfirst("target")
+ if not targetname:
+ raise CgiError, "No target selected!"
+ for target in get_targets():
+ if target.name == targetname:
+ break
+ else:
+ raise CgiError, "Target not found!"
+ try:
+ tmp = int(packagerev)
+ except ValueError:
+ raise CgiError, "Invalid revision provided!"
+ for allowed in target.allowed:
+ if packageurl.startswith(allowed):
+ break
+ else:
+ raise CgiError, "%s is not allowed for this target!" % packageurl
+ get_srpm(packageurl,
+ revision=packagerev,
+ targetdirs=target.target,
+ packager=packager,
+ revname=1,
+ svnlog=1,
+ scripts=target.scripts)
+ show("Package submitted!")
+
+def main():
+ if not os.environ.has_key('REQUEST_METHOD'):
+ sys.stderr.write("error: this program is meant to be used as a cgi\n")
+ sys.exit(1)
+ print "Content-type: text/html\n\n"
+ try:
+ username = os.environ.get("REMOTE_USER")
+ method = os.environ.get("REQUEST_METHOD")
+ if not username or method != "POST":
+ show()
+ else:
+ useremail = config.get("users", username)
+ if not useremail:
+ raise CgiError, \
+ "Your email was not found. Contact the administrator!"
+ submit_packages(useremail)
+ except CgiError, e:
+ show(str(e), error=1)
+ except Error, e:
+ error = str(e)
+ show(error[0].upper()+error[1:], error=1)
+ except:
+ cgi.print_exception()
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/cgi/xmlrpcserver.py b/RepSys/cgi/xmlrpcserver.py
new file mode 100644
index 0000000..e0851d1
--- /dev/null
+++ b/RepSys/cgi/xmlrpcserver.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys.rpmutil import get_srpm
+from RepSys.cgiutil import CgiError, get_targets
+import sys
+import os
+
+import xmlrpclib, cgi
+
+class XmlRpcIface:
+ def author_email(self, author):
+ return config.get("users", author)
+
+ def submit_package(self, packageurl, packagerev, targetname):
+ username = os.environ.get("REMOTE_USER")
+ packager = config.get("users", username)
+ if not packager:
+ raise CgiError, "your email was not found"
+ elif not packagerev:
+ raise CgiError, "no revision provided"
+ elif not targetname:
+ raise CgiError, "no target provided"
+ else:
+ targetname = targetname.lower()
+ for target in get_targets():
+ if target.name.lower() == targetname:
+ break
+ else:
+ raise CgiError, "target not found"
+ try:
+ tmp = int(packagerev)
+ except ValueError:
+ raise CgiError, "invalid revision provided"
+ for allowed in target.allowed:
+ if packageurl.startswith(allowed):
+ break
+ else:
+ raise CgiError, "%s is not allowed for this target" \
+ % packageurl
+ get_srpm(packageurl,
+ revision=packagerev,
+ targetdirs=target.target,
+ packager=packager,
+ revname=1,
+ svnlog=1,
+ scripts=target.scripts)
+ return 1
+
+ def submit_targets(self):
+ return [x.name for x in get_targets()]
+
+TEMPLATE = """\
+Content-type: text/html
+
+<html>
+<head>
+<title>Repository system SOAP server</title>
+</head>
+<body bgcolor="white">
+<br>
+<hr>
+<center>
+<b>%(message)s</b>
+</center>
+<hr>
+</body>
+</html>
+"""
+
+def show(msg="", error=0):
+ if error:
+ msg = '<font color="red">%s</font>' % msg
+ print TEMPLATE % {"message":msg}
+
+def main():
+ if not os.environ.has_key('REQUEST_METHOD'):
+ sys.stderr.write("error: this program is meant to be used as a cgi\n")
+ sys.exit(1)
+ username = os.environ.get("REMOTE_USER")
+ method = os.environ.get("REQUEST_METHOD")
+ if not username or method != "POST":
+ show("This is a XMLRPC interface!", error=1)
+ sys.exit(1)
+
+ iface = XmlRpcIface()
+
+ response = ""
+ try:
+ form = cgi.FieldStorage()
+ parms, method = xmlrpclib.loads(form.value)
+ meth = getattr(iface, method)
+ response = (meth(*parms),)
+ except CgiError, e:
+ msg = str(e)
+ try:
+ msg = msg.decode("iso-8859-1")
+ except UnicodeError:
+ pass
+ response = xmlrpclib.Fault(1, msg)
+ except Exception, e:
+ msg = str(e)
+ try:
+ msg = msg.decode("iso-8859-1")
+ except UnicodeError:
+ pass
+ response = xmlrpclib.Fault(1, msg)
+
+ sys.stdout.write("Content-type: text/xml\n\n")
+ sys.stdout.write(xmlrpclib.dumps(response, methodresponse=1))
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/cgiutil.py b/RepSys/cgiutil.py
new file mode 100644
index 0000000..35c5efb
--- /dev/null
+++ b/RepSys/cgiutil.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys.svn import SVN
+from RepSys.ConfigParser import NoSectionError
+import time
+import re
+
+class CgiError(Error): pass
+
+class SubmitTarget:
+ def __init__(self):
+ self.name = ""
+ self.target = ""
+ self.macros = []
+ self.allowed = []
+ self.scripts = []
+
+TARGETS = []
+
+def parse_macrosref(refs, config):
+ macros = []
+ for name in refs:
+ secname = "macros %s" % name
+ try:
+ macros.extend(config.walk(secname, raw=True))
+ except NoSectionError:
+ raise Error, "missing macros section " \
+ "%r in configuration" % secname
+ return macros
+
+def get_targets():
+ global TARGETS
+ if not TARGETS:
+ target = SubmitTarget()
+ targetoptions = {}
+ submit_re = re.compile("^submit\s+(.+)$")
+ for section in config.sections():
+ m = submit_re.match(section)
+ if m:
+ target = SubmitTarget()
+ target.name = m.group(1)
+ for option, value in config.walk(section):
+ if option in ("target", "allowed", "scripts"):
+ setattr(target, option, value.split())
+ elif option == "rpm-macros":
+ refs = value.split()
+ target.macros = parse_macrosref(refs, config)
+ else:
+ raise Error, "unknown [%s] option %s" % (section, option)
+ TARGETS.append(target)
+ return TARGETS
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/command.py b/RepSys/command.py
new file mode 100644
index 0000000..63f2df9
--- /dev/null
+++ b/RepSys/command.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+from RepSys import SilentError, Error, config
+import sys, os
+import urlparse
+import optparse
+
+__all__ = ["OptionParser", "do_command", "default_parent"]
+
+class CapitalizeHelpFormatter(optparse.IndentedHelpFormatter):
+
+ def format_usage(self, usage):
+ return optparse.IndentedHelpFormatter \
+ .format_usage(self, usage).capitalize()
+
+ def format_heading(self, heading):
+ return optparse.IndentedHelpFormatter \
+ .format_heading(self, heading).capitalize()
+
+class OptionParser(optparse.OptionParser):
+
+ def __init__(self, usage=None, help=None, **kwargs):
+ if not "formatter" in kwargs:
+ kwargs["formatter"] = CapitalizeHelpFormatter()
+ optparse.OptionParser.__init__(self, usage, **kwargs)
+ self._overload_help = help
+
+ def format_help(self, formatter=None):
+ if self._overload_help:
+ return self._overload_help
+ else:
+ return optparse.OptionParser.format_help(self, formatter)
+
+ def error(self, msg):
+ raise Error, msg
+
+def do_command(parse_options_func, main_func):
+ try:
+ opt = parse_options_func()
+ main_func(**opt.__dict__)
+ except SilentError:
+ sys.exit(1)
+ except Error, e:
+ sys.stderr.write("error: %s\n" % str(e))
+ sys.exit(1)
+ except KeyboardInterrupt:
+ sys.stderr.write("interrupted\n")
+ sys.stderr.flush()
+ sys.exit(1)
+
+def default_parent(url):
+ if url.find("://") == -1:
+ default_parent = config.get("global", "default_parent")
+ if not default_parent:
+ raise Error, "received a relative url, " \
+ "but default_parent was not setup"
+ parsed = list(urlparse.urlparse(default_parent))
+ parsed[2] = os.path.normpath(parsed[2] + "/" + url)
+ url = urlparse.urlunparse(parsed)
+ return url
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/__init__.py b/RepSys/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/RepSys/commands/__init__.py
diff --git a/RepSys/commands/authoremail.py b/RepSys/commands/authoremail.py
new file mode 100644
index 0000000..f5b8b70
--- /dev/null
+++ b/RepSys/commands/authoremail.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys.command import *
+import sys
+import getopt
+
+HELP = """\
+Usage: repsys authoremail [OPTIONS] AUTHOR
+
+Shows the e-mail of an SVN author. It is just a simple interface to access
+the [authors] section of repsys.conf.
+
+Options:
+ -h Show this message
+
+Examples:
+ repsys authoremail john
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.author = args[0]
+ return opts
+
+def print_author_email(author):
+ email = config.get("users", author)
+ if not email:
+ raise Error, "author not found"
+ print email
+
+def main():
+ do_command(parse_options, print_author_email)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/changed.py b/RepSys/commands/changed.py
new file mode 100644
index 0000000..66c1a53
--- /dev/null
+++ b/RepSys/commands/changed.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+from RepSys import Error, disable_mirror
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.rpmutil import check_changed
+import getopt
+import sys
+
+HELP = """\
+Usage: repsys changed [OPTIONS] URL
+
+Shows if there are pending changes since the last package release.
+
+Options:
+ -a Check all packages in given URL
+ -s Show differences
+ -M Do not use the mirror (use the main repository)
+ -h Show this message
+
+Examples:
+ repsys changed http://repos/svn/cnc/snapshot/foo
+ repsys changed -a http://repos/svn/cnc/snapshot
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-a", dest="all", action="store_true")
+ parser.add_option("-s", dest="show", action="store_true")
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ opts, args = parser.parse_args()
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(args[0])
+ opts.verbose = 1 # Unconfigurable
+ return opts
+
+def main():
+ do_command(parse_options, check_changed)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/ci.py b/RepSys/commands/ci.py
new file mode 100644
index 0000000..8d373b5
--- /dev/null
+++ b/RepSys/commands/ci.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+from RepSys.command import *
+from RepSys.rpmutil import commit
+
+HELP = """\
+Usage: repsys ci [TARGET]
+
+Will commit recent modifications in the package.
+
+The difference between an ordinary "svn ci" and "repsys ci" is that it
+relocates the working copy to the default repository in case the option
+"mirror" is set in repsys.conf.
+
+Options:
+ -h Show this message
+ -m MSG Use the MSG as the log message
+ -F FILE Read log message from FILE
+
+Examples:
+ repsys ci
+ repsys ci SPECS/package.spec SPECS/package-patch.patch
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-m", dest="message", default=None)
+ parser.add_option("-F", dest="logfile", type="string",
+ default=None)
+ opts, args = parser.parse_args()
+ if len(args):
+ opts.target = args[0]
+ return opts
+
+def main():
+ do_command(parse_options, commit)
diff --git a/RepSys/commands/co.py b/RepSys/commands/co.py
new file mode 100644
index 0000000..81e4140
--- /dev/null
+++ b/RepSys/commands/co.py
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+from RepSys import Error, disable_mirror
+from RepSys.command import *
+from RepSys.rpmutil import checkout
+import getopt
+import sys
+
+HELP = """\
+Usage: repsys co [OPTIONS] URL [LOCALPATH]
+
+Checkout the package source from the Mandriva repository.
+
+If the 'mirror' option is enabled, the package is obtained from the mirror
+repository.
+
+You can specify the distro branch to checkout from by using distro/pkgname.
+
+Options:
+ -d The distribution branch to checkout from
+ -b The package branch
+ -r REV Revision to checkout
+ -S Do not download sources from the binaries repository
+ -L Do not make symlinks of the binaries downloaded in SOURCES/
+ -s Only checkout the SPECS/ directory
+ -M Do not use the mirror (use the main repository)
+ --check Check integrity of files fetched from the binary repository
+ -h Show this message
+
+Examples:
+ repsys co pkgname
+ repsys co -d 2009.0 pkgname
+ repsys co 2009.0/pkgame
+ repsys co http://repos/svn/cnc/snapshot/foo
+ repsys co http://repos/svn/cnc/snapshot/foo foo-pkg
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-r", dest="revision")
+ parser.add_option("-S", dest="use_binrepo", default=True,
+ action="store_false")
+ parser.add_option("--check", dest="binrepo_check", default=False,
+ action="store_true")
+ parser.add_option("-L", dest="binrepo_link", default=True,
+ action="store_false")
+ parser.add_option("--distribution", "-d", dest="distro", default=None)
+ parser.add_option("--branch", "-b", dest="branch", default=None)
+ parser.add_option("-s", "--spec", dest="spec", default=False,
+ action="store_true")
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ opts, args = parser.parse_args()
+ if len(args) not in (1, 2):
+ raise Error, "invalid arguments"
+ # here we don't use package_url in order to notify the user we are
+ # using the mirror
+ opts.pkgdirurl = args[0]
+ if len(args) == 2:
+ opts.path = args[1]
+ else:
+ opts.path = None
+ return opts
+
+def main():
+ do_command(parse_options, checkout)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/create.py b/RepSys/commands/create.py
new file mode 100644
index 0000000..ded8abe
--- /dev/null
+++ b/RepSys/commands/create.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+from RepSys import Error
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.rpmutil import create_package
+import getopt
+import sys
+
+HELP = """\
+Usage: repsys create [OPTIONS] URL
+
+Creates the minimal structure of a package in the repository.
+
+Options:
+ -h Show this message
+
+Examples:
+ repsys create newpkg
+ repsys create svn+ssh://svn.mandriva.com/svn/packages/cooker/newpkg
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(args[0], mirrored=False)
+ opts.verbose = 1 # Unconfigurable
+ return opts
+
+def main():
+ do_command(parse_options, create_package)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/del.py b/RepSys/commands/del.py
new file mode 100644
index 0000000..2c6902e
--- /dev/null
+++ b/RepSys/commands/del.py
@@ -0,0 +1,30 @@
+from RepSys import Error
+from RepSys.command import *
+from RepSys.rpmutil import binrepo_delete
+
+HELP = """\
+Usage: repsys del [OPTIONS] [PATH]
+
+Remove a given file from the binary sources repository.
+
+Changes in the sources file will be left uncommited.
+
+Options:
+ -c automatically commit the 'sources' file
+ -h help
+
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-c", dest="commit", default=False,
+ action="store_true")
+ opts, args = parser.parse_args()
+ if len(args):
+ opts.paths = args
+ else:
+ raise Error, "you need to provide a path"
+ return opts
+
+def main():
+ do_command(parse_options, binrepo_delete)
diff --git a/RepSys/commands/editlog.py b/RepSys/commands/editlog.py
new file mode 100644
index 0000000..9d1afc5
--- /dev/null
+++ b/RepSys/commands/editlog.py
@@ -0,0 +1,39 @@
+#!/usr/bin/python
+from RepSys import Error
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.svn import SVN
+import re
+
+HELP = """\
+Usage: repsys editlog [OPTIONS] [URL] REVISION
+
+Options:
+ -h Show this message
+
+Examples:
+ repsys editlog 14800
+ repsys editlog https://repos/svn/cnc/snapshot 14800
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ if len(args) == 2:
+ pkgdirurl, revision = args
+ elif len(args) == 1:
+ pkgdirurl, revision = "", args[0]
+ else:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(pkgdirurl, mirrored=False)
+ opts.revision = re.compile(r".*?(\d+).*").sub(r"\1", revision)
+ return opts
+
+def editlog(pkgdirurl, revision):
+ svn = SVN()
+ svn.propedit("svn:log", pkgdirurl, rev=revision)
+
+def main():
+ do_command(parse_options, editlog)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/getspec.py b/RepSys/commands/getspec.py
new file mode 100644
index 0000000..a357ef9
--- /dev/null
+++ b/RepSys/commands/getspec.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+from RepSys import Error, disable_mirror
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.rpmutil import get_spec
+import getopt
+import sys
+
+HELP = """\
+Usage: repsys getspec [OPTIONS] REPPKGURL
+
+Prints the .spec file of a given package.
+
+Options:
+ -t DIR Use DIR as target for spec file (default is ".")
+ -M Do not use the mirror (use the main repository)
+ -h Show this message
+
+Examples:
+ repsys getspec pkgname
+ repsys getspec svn+ssh://svn.mandriva.com/svn/packages/cooker/pkgname
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-t", dest="targetdir", default=".")
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ opts, args = parser.parse_args()
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(args[0])
+ return opts
+
+def main():
+ do_command(parse_options, get_spec)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/getsrpm.py b/RepSys/commands/getsrpm.py
new file mode 100644
index 0000000..1767bb7
--- /dev/null
+++ b/RepSys/commands/getsrpm.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+#
+# This program will extract given version/revision of the named package
+# from the Conectiva Linux repository system.
+#
+from RepSys import Error, config, disable_mirror
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.rpmutil import get_srpm
+import tempfile
+import shutil
+import getopt
+import glob
+import sys
+import os
+
+HELP = """\
+Usage: repsys getsrpm [OPTIONS] REPPKGURL
+
+Generates the source RPM (.srpm) file of a given package.
+
+Options:
+ -c Use files in current/ directory (default)
+ -p Use files in pristine/ directory
+ -v VER Use files from the version specified by VER (e.g. 2.2.1-2cl)
+ -r REV Use files from current directory, in revision REV (e.g. 1001)
+ -t DIR Put SRPM file in directory DIR when done (default is ".")
+ -P USER Define the RPM packager inforamtion to USER
+ -s FILE Run script with "FILE TOPDIR SPECFILE" command
+ -n Rename the package to include the revision number
+ -l Use subversion log to build rpm %changelog
+ -T FILE Template to be used to generate the %changelog
+ -M Do not use the mirror (use the main repository)
+ -h Show this message
+ -S Do not download sources from the binary repository
+ --check Check integrity of files fetched from the binary repository
+ --strict Check if the given revision contains changes in REPPKGURL
+
+Examples:
+ repsys getsrpm python
+ repsys getsrpm -l python
+ repsys getsrpm http://foo.bar/svn/cnc/snapshot/python
+ repsys getsrpm -p http://foo.bar/svn/cnc/releases/8cl/python
+ repsys getsrpm -r 1001 file:///svn/cnc/snapshot/python
+"""
+
+def mode_callback(option, opt, val, parser, mode):
+ opts = parser.values
+ opts.mode = mode
+ if mode == "version":
+ try:
+ opts.version, opts.release = val.split("-", 1)
+ except ValueError:
+ raise Error, "wrong version, use something like 2.2-1mdk"
+ elif mode == "revision":
+ opts.revision = val
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.defaults["mode"] = "current"
+ parser.defaults["version"] = None
+ parser.defaults["release"] = None
+ parser.defaults["revision"] = None
+ parser.defaults["submit"] = False
+ callback_options = dict(action="callback", callback=mode_callback,
+ type="string", dest="__ignore")
+ parser.add_option("-c", callback_kwargs={"mode": "current"}, nargs=0,
+ **callback_options)
+ parser.add_option("-p", callback_kwargs={"mode": "pristine"}, nargs=0,
+ **callback_options)
+ parser.add_option("-r", callback_kwargs={"mode": "revision"}, nargs=1,
+ **callback_options)
+ parser.add_option("-v", callback_kwargs={"mode": "version"}, nargs=1,
+ **callback_options)
+ parser.add_option("-t", dest="targetdirs", action="append", default=[])
+ parser.add_option("-s", dest="scripts", action="append", default=[])
+ parser.add_option("-P", dest="packager", default="")
+ parser.add_option("-n", dest="revname", action="store_true")
+ parser.add_option("-l", dest="svnlog", action="store_true")
+ parser.add_option("-T", dest="template", type="string", default=None)
+ parser.add_option("-S", dest="use_binrepo", default=True,
+ action="store_false")
+ parser.add_option("--check", dest="binrepo_check", default=False,
+ action="store_true")
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ parser.add_option("--strict", dest="strict", default=False,
+ action="store_true")
+ opts, args = parser.parse_args()
+ del opts.__ignore
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(args[0])
+ opts.verbose = 1
+ return opts
+
+def main():
+ do_command(parse_options, get_srpm)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/log.py b/RepSys/commands/log.py
new file mode 100644
index 0000000..28df27d
--- /dev/null
+++ b/RepSys/commands/log.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+from RepSys import config, mirror, disable_mirror
+from RepSys.command import *
+from RepSys.layout import package_url, checkout_url
+from RepSys.rpmutil import sync
+from RepSys.util import execcmd
+import sys
+import os
+
+HELP = """\
+Usage: repsys log [OPTIONS] [PACKAGE]
+
+Shows the SVN log for a given package.
+
+Options:
+ -h Show this message
+ -v Show changed paths
+ -l LIMIT Limit of log entries to show
+ -r REV Show a specific revision
+ -M Do not use the mirror (use the main repository)
+
+Examples:
+ repsys log mutt
+ repsys log 2009.1/mutt
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-v", dest="verbose", action="store_true",
+ default=False)
+ parser.add_option("-l", "--limit", dest="limit", type="int",
+ default=None)
+ parser.add_option("-r", dest="revision", type="string", default=None)
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ opts, args = parser.parse_args()
+ if len(args):
+ opts.pkgdirurl = package_url(args[0])
+ else:
+ parser.error("log requires a package name")
+ return opts
+
+def svn_log(pkgdirurl, verbose=False, limit=None, revision=None):
+ mirror.info(pkgdirurl)
+ url = checkout_url(pkgdirurl)
+ svncmd = config.get("global", "svn-command", "svn")
+ args = [svncmd, "log", url]
+ if verbose:
+ args.append("-v")
+ if limit:
+ args.append("-l")
+ args.append(limit)
+ if revision:
+ args.append("-r")
+ args.append(revision)
+ if os.isatty(sys.stdin.fileno()):
+ args.append("| less")
+ rawcmd = " ".join(args)
+ execcmd(rawcmd, show=True)
+
+def main():
+ do_command(parse_options, svn_log)
diff --git a/RepSys/commands/markrelease.py b/RepSys/commands/markrelease.py
new file mode 100644
index 0000000..057cf1d
--- /dev/null
+++ b/RepSys/commands/markrelease.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+#
+# This program will append a release to the Conectiva Linux package
+# repository system. It's meant to be a startup system to include
+# pre-packaged SRPMS in the repository, thus, you should not commit
+# packages over an ongoing package structure (with changes in current/
+# directory and etc). Also, notice that packages must be included in
+# cronological order.
+#
+from RepSys import Error
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.simplerpm import SRPM
+from RepSys.rpmutil import mark_release
+from RepSys.util import get_auth
+import getopt
+import sys
+import os
+
+HELP = """\
+*** WARNING --- You probably SHOULD NOT use this program! --- WARNING ***
+
+Usage: repsys markrelease [OPTIONS] REPPKGURL
+
+This subcommand creates a 'tag' for a given revision of a given package.
+
+The tag will be stored in the directory releases/ inside the package
+structure.
+
+Options:
+ -f FILE Try to extract information from given file
+ -r REV Revision which will be used to make the release copy tag
+ -v VER Version which will be used to make the release copy tag
+ -n Append package name to provided URL
+ -h Show this message
+
+Examples:
+ repsys markrelease -r 68 -v 1.0-1 file://svn/cnc/snapshot/foo
+ repsys markrelease -f @68:foo-1.0-1.src.rpm file://svn/cnc/snapshot/foo
+ repsys markrelease -r 68 -f foo-1.0.src.rpm file://svn/cnc/snapshot/foo
+"""
+
+def version_callback(option, opt, val, parser):
+ opts = parser.values
+ try:
+ opts.version, opts.release = val.split("-", 1)
+ except ValueError:
+ raise Error, "wrong version, use something like 1:2.2-1mdk"
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.defaults["version"] = None
+ parser.defaults["release"] = None
+ parser.add_option("-v", action="callback", callback=version_callback,
+ nargs=1, type="string", dest="__ignore")
+ parser.add_option("-r", dest="revision")
+ parser.add_option("-f", dest="filename")
+ parser.add_option("-n", dest="appendname", action="store_true")
+ opts, args = parser.parse_args()
+
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+
+ opts.pkgdirurl = package_url(args[0], mirrored=False)
+
+ filename = opts.filename
+ appendname = opts.appendname
+ del opts.filename, opts.appendname, opts.__ignore
+
+ if filename:
+ if not os.path.isfile(filename):
+ raise Error, "file not found: "+filename
+ if not opts.revision:
+ basename = os.path.basename(filename)
+ end = basename.find(":")
+ if basename[0] != "@" or end == -1:
+ raise Error, "couldn't guess revision from filename"
+ opts.revision = basename[1:end]
+ srpm = None
+ if not opts.version:
+ srpm = SRPM(filename)
+ if srpm.epoch:
+ opts.version = "%s:%s" % (srpm.epoch, srpm.version)
+ else:
+ opts.version = srpm.version
+ opts.release = srpm.release
+ if appendname:
+ if not srpm:
+ srpm = SRPM(filename)
+ opts.pkgdirurl = "/".join([opts.pkgdirurl, srpm.name])
+ elif appendname:
+ raise Error, "option -n requires option -f"
+ elif not opts.revision:
+ raise Error, "no revision provided"
+ elif not opts.version:
+ raise Error, "no version provided"
+ #get_auth()
+ return opts
+
+def main():
+ do_command(parse_options, mark_release)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/patchspec.py b/RepSys/commands/patchspec.py
new file mode 100644
index 0000000..9a4881b
--- /dev/null
+++ b/RepSys/commands/patchspec.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+#
+# This program will try to patch a spec file from a given package url.
+#
+from RepSys import Error
+from RepSys.rpmutil import patch_spec
+from RepSys.command import *
+from RepSys.layout import package_url
+import getopt
+import sys
+
+HELP = """\
+Usage: repsys patchspec [OPTIONS] REPPKGURL PATCHFILE
+
+It will try to patch a spec file from a given package url.
+
+Options:
+ -l LOG Use LOG as log message
+ -h Show this message
+
+Examples:
+ repsys patchspec http://repos/svn/cnc/snapshot/foo
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-l", dest="log", default="")
+ opts, args = parser.parse_args()
+ if len(args) != 2:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = package_url(args[0], mirrored=False)
+ opts.patchfile = args[1]
+ return opts
+
+def main():
+ do_command(parse_options, patch_spec)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/putsrpm.py b/RepSys/commands/putsrpm.py
new file mode 100644
index 0000000..efe1a15
--- /dev/null
+++ b/RepSys/commands/putsrpm.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+from RepSys import Error
+from RepSys.command import *
+from RepSys.layout import package_url
+from RepSys.rpmutil import put_srpm
+import getopt
+import sys, os
+
+HELP = """\
+Usage: repsys putsrpm [OPTIONS] SOURCERPMS
+
+Will import source RPMs into the SVN repository.
+
+If the package was already imported, it will add the new files and remove
+those not present in the source RPM.
+
+Options:
+ -m LOG Log message used when commiting changes
+ -t Create version-release tag on releases/
+ -b NAME The distribution branch to place it
+ -d URL The URL of base directory where packages will be placed
+ -c URL The URL of the base directory where the changelog will be
+ placed
+ -s Don't strip the changelog from the spec
+ (nor import it into misc/)
+ -n Don't try to rename the spec file
+ -h Show this message
+
+Examples:
+ repsys putsrpm pkg/SRPMS/pkg-2.0-1.src.rpm
+ repsys putsrpm -b 2009.1 foo-1.1-1.src.rpm
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-l", dest="logmsg", default="")
+ parser.add_option("-t", dest="markrelease", action="store_true",
+ default=False)
+ parser.add_option("-s", dest="striplog", action="store_false",
+ default=True)
+ parser.add_option("-b", dest="branch", type="string", default=None)
+ parser.add_option("-d", dest="baseurl", type="string", default=None)
+ parser.add_option("-c", dest="baseold", type="string", default=None)
+ parser.add_option("-n", dest="rename", action="store_false",
+ default=True)
+ opts, args = parser.parse_args()
+ opts.srpmfiles = args
+ return opts
+
+def put_srpm_cmd(srpmfiles, markrelease=False, striplog=True, branch=None,
+ baseurl=None, baseold=None, logmsg=None, rename=False):
+ for path in srpmfiles:
+ put_srpm(path, markrelease, striplog, branch, baseurl, baseold,
+ logmsg, rename)
+
+def main():
+ do_command(parse_options, put_srpm_cmd)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/rpmlog.py b/RepSys/commands/rpmlog.py
new file mode 100644
index 0000000..238b675
--- /dev/null
+++ b/RepSys/commands/rpmlog.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+#
+# This program will convert the output of "svn log" to be suitable
+# for usage in an rpm %changelog session.
+#
+from RepSys import Error, layout, disable_mirror
+from RepSys.command import *
+from RepSys.svn import SVN
+from RepSys.log import get_changelog, split_spec_changelog
+from cStringIO import StringIO
+import getopt
+import os
+import sys
+
+HELP = """\
+Usage: repsys rpmlog [OPTIONS] REPPKGDIRURL
+
+Prints the RPM changelog of a given package.
+
+Options:
+ -r REV Collect logs from given revision to revision 0
+ -n NUM Output only last NUM entries
+ -T FILE %changelog template file to be used
+ -o Append old package changelog
+ -p Append changelog found in .spec file
+ -s Sort changelog entries, even from the old log
+ -M Do not use the mirror (use the main repository)
+ -h Show this message
+
+Examples:
+ repsys rpmlog python
+ repsys rpmlog http://svn.mandriva.com/svn/packages/cooker/python
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("-r", dest="revision")
+ parser.add_option("-n", dest="size", type="int")
+ parser.add_option("-T", "--template", dest="template", type="string")
+ parser.add_option("-o", dest="oldlog", default=False,
+ action="store_true")
+ parser.add_option("-p", dest="usespec", default=False,
+ action="store_true")
+ parser.add_option("-s", dest="sort", default=False,
+ action="store_true")
+ parser.add_option("-M", "--no-mirror", action="callback",
+ callback=disable_mirror)
+ opts, args = parser.parse_args()
+ if len(args) != 1:
+ raise Error, "invalid arguments"
+ opts.pkgdirurl = layout.package_url(args[0])
+ return opts
+
+def rpmlog(pkgdirurl, revision, size, template, oldlog, usespec, sort):
+ another = None
+ if usespec:
+ svn = SVN()
+ specurl = layout.package_spec_url(pkgdirurl)
+ rawspec = svn.cat(specurl, rev=revision)
+ spec, another = split_spec_changelog(StringIO(rawspec))
+ newlog = get_changelog(pkgdirurl, another=another, rev=revision,
+ size=size, sort=sort, template=template, oldlog=oldlog)
+ sys.stdout.writelines(newlog)
+
+def main():
+ do_command(parse_options, rpmlog)
+
+# vim:sw=4:ts=4:et
diff --git a/RepSys/commands/submit.py b/RepSys/commands/submit.py
new file mode 100644
index 0000000..2924329
--- /dev/null
+++ b/RepSys/commands/submit.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+from RepSys import Error, config, layout, mirror
+from RepSys.svn import SVN
+from RepSys.command import *
+from RepSys.rpmutil import get_spec, get_submit_info
+from RepSys.util import get_auth, execcmd, get_helper
+import urllib
+import getopt
+import sys
+import re
+import subprocess
+import uuid
+
+import xmlrpclib
+
+HELP = """\
+Usage: repsys submit [OPTIONS] [URL[@REVISION] ...]
+
+Submits the package from URL to the submit host.
+
+The submit host will try to build the package, and upon successful
+completion will 'tag' the package and upload it to the official
+repositories.
+
+The package name can refer to an alias to a group of packages defined in
+the section submit-groups of the configuration file.
+
+The status of the submit can visualized at:
+
+http://kenobi.mandriva.com/bs/output.php
+
+If no URL and revision are specified, the latest changed revision in the
+package working copy of the current directory will be used.
+
+Options:
+ -t TARGET Submit given package URL to given target
+ -l Just list available targets
+ -r REV Provides a revision number (when not providing as an
+ argument)
+ -s The host in which the package URL will be submitted
+ (defaults to the host in the URL)
+ -a Submit all URLs at once (depends on server-side support)
+ -i SID Use the submit identifier SID
+ -h Show this message
+ --distro The distribution branch where the packages come from
+ --define Defines one variable to be used by the submit scripts
+ in the submit host
+
+Examples:
+ repsys submit
+ repsys submit foo
+ repsys submit 2009.1/foo
+ repsys submit foo@14800 bar baz@11001
+ repsys submit https://repos/svn/mdv/cooker/foo
+ repsys submit -l https://repos
+ repsys submit 2008.1/my-packages@11011
+ repsys submit --define section=main/testing -t 2008.1
+"""
+
+DEFAULT_TARGET = "Cooker"
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.defaults["revision"] = None
+ parser.add_option("-t", dest="target", default=None)
+ parser.add_option("-l", action="callback", callback=list_targets)
+ parser.add_option("-r", dest="revision", type="string", nargs=1)
+ parser.add_option("-s", dest="submithost", type="string", nargs=1,
+ default=None)
+ parser.add_option("-i", dest="sid", type="string", nargs=1,
+ default=None)
+ parser.add_option("-a", dest="atonce", action="store_true", default=False)
+ parser.add_option("--distro", dest="distro", type="string",
+ default=None)
+ parser.add_option("--define", action="append", default=[])
+ opts, args = parser.parse_args()
+ if not args:
+ name, url, rev = get_submit_info(".")
+ args = ["%s@%s" % (url, str(rev))]
+ print "Submitting %s at revision %s" % (name, rev)
+ print "URL: %s" % url
+ if opts.revision is not None:
+ # backwards compatibility with the old -r usage
+ if len(args) == 1:
+ args[0] = args[0] + "@" + opts.revision
+ else:
+ raise Error, "can't use -r REV with more than one package name"
+ del opts.revision
+ if len(args) == 2:
+ # prevent from using the old <name> <rev> syntax
+ try:
+ rev = int(args[1])
+ except ValueError:
+ # ok, it is a package name, let it pass
+ pass
+ else:
+ raise Error, "the format <name> <revision> is deprecated, "\
+ "use <name>@<revision> instead"
+ # expand group aliases
+ expanded = []
+ for nameurl in args:
+ expanded.extend(expand_group(nameurl))
+ if expanded != args:
+ print "Submitting: %s" % " ".join(expanded)
+ args = expanded
+ # generate URLs for package names:
+ opts.urls = [mirror.strip_username(
+ layout.package_url(nameurl, distro=opts.distro, mirrored=False))
+ for nameurl in args]
+ # find the revision if not specified:
+ newurls = []
+ for url in opts.urls:
+ if not "@" in url:
+ print "Fetching revision..."
+ courl = layout.checkout_url(url)
+ log = SVN().log(courl, limit=1)
+ if not log:
+ raise Error, "can't find a revision for %s" % courl
+ ci = log[0]
+ print "URL:", url
+ print "Commit:",
+ print "%d | %s" % (ci.revision, ci.author),
+ if ci.lines:
+ line = " ".join(ci.lines).strip()
+ if len(line) > 57:
+ line = line[:57] + "..."
+ print "| %s" % line,
+ print
+ url = url + "@" + str(ci.revision)
+ newurls.append(url)
+ opts.urls[:] = newurls
+ # choose a target if not specified:
+ if opts.target is None and opts.distro is None:
+ target = layout.distro_branch(opts.urls[0]) or DEFAULT_TARGET
+ print "Implicit target: %s" % target
+ opts.target = target
+ del opts.distro
+ return opts
+
+def expand_group(group):
+ name, rev = layout.split_url_revision(group)
+ distro = None
+ if "/" in name:
+ distro, name = name.rsplit("/", 1)
+ found = config.get("submit-groups", name)
+ packages = [group]
+ if found:
+ packages = found.split()
+ if rev:
+ packages = [("%s@%s" % (package, rev))
+ for package in packages]
+ if distro:
+ packages = ["%s/%s" % (distro, package)
+ for package in packages]
+ return packages
+
+def list_targets(option, opt, val, parser):
+ host = config.get("submit", "host")
+ if host is None:
+ raise Error, "no submit host defined in repsys.conf"
+ createsrpm = get_helper("create-srpm")
+ #TODO make it configurable
+ command = "ssh %s %s --list" % (host, createsrpm)
+ execcmd(command, show=True)
+ sys.exit(0)
+
+def submit(urls, target, define=[], submithost=None, atonce=False, sid=None):
+ if submithost is None:
+ submithost = config.get("submit", "host")
+ if submithost is None:
+ # extract the submit host from the svn host
+ type, rest = urllib.splittype(pkgdirurl)
+ host, path = urllib.splithost(rest)
+ user, host = urllib.splituser(host)
+ submithost, port = urllib.splitport(host)
+ del type, user, port, path, rest
+ # runs a create-srpm in the server through ssh, which will make a
+ # copy of the rpm in the export directory
+ createsrpm = get_helper("create-srpm")
+ baseargs = ["ssh", submithost, createsrpm, "-t", target]
+ if not sid:
+ sid = uuid.uuid4()
+ define.append("sid=%s" % sid)
+ for entry in reversed(define):
+ baseargs.append("--define")
+ baseargs.append(entry)
+ cmdsargs = []
+ if len(urls) == 1:
+ # be compatible with server-side repsys versions older than 1.6.90
+ url, rev = layout.split_url_revision(urls[0])
+ baseargs.append("-r")
+ baseargs.append(str(rev))
+ baseargs.append(url)
+ cmdsargs.append(baseargs)
+ elif atonce:
+ cmdsargs.append(baseargs + urls)
+ else:
+ cmdsargs.extend((baseargs + [url]) for url in urls)
+ for cmdargs in cmdsargs:
+ command = subprocess.list2cmdline(cmdargs)
+ status, output = execcmd(command)
+ if status == 0:
+ print "Package submitted!"
+ else:
+ sys.stderr.write(output)
+ sys.exit(status)
+
+def main():
+ do_command(parse_options, submit)
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/commands/switch.py b/RepSys/commands/switch.py
new file mode 100644
index 0000000..998ae2c
--- /dev/null
+++ b/RepSys/commands/switch.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+from RepSys.command import *
+from RepSys.rpmutil import switch
+
+HELP = """\
+Usage: repsys switch [URL]
+
+Relocates the working copy to the base location URL.
+
+If URL is not provided, it will use the option repository from repsys.conf
+as default, or, if the current working copy is already based in
+default_parent, it will use the location from the mirror option from
+repsys.conf.
+
+If the current work is based in another URL, it will use default_parent.
+
+Options:
+ -h Show this message
+
+Examples:
+ repsys switch
+ repsys switch https://mirrors.localnetwork/svn/packages/
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ if len(args):
+ opts.mirrorurl = args[0]
+ return opts
+
+def main():
+ do_command(parse_options, switch)
diff --git a/RepSys/commands/sync.py b/RepSys/commands/sync.py
new file mode 100644
index 0000000..b4bdaba
--- /dev/null
+++ b/RepSys/commands/sync.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+from RepSys.command import *
+from RepSys.rpmutil import sync
+
+HELP = """\
+Usage: repsys sync
+
+Will add or remove from the working copy those files added or removed
+in the spec file.
+
+It will not commit the changes.
+
+Options:
+ -c Commit the changes, as in ci
+ --dry-run Print results without changing the working copy
+ --download -d
+ Try to download the source files not found
+ -h Show this message
+
+Examples:
+ repsys sync
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ parser.add_option("--dry-run", dest="dryrun", default=False,
+ action="store_true")
+ parser.add_option("-c", dest="ci", default=False,
+ action="store_true")
+ parser.add_option("-d", "--download", dest="download", default=False,
+ action="store_true")
+ opts, args = parser.parse_args()
+ if len(args):
+ opts.target = args[0]
+ return opts
+
+def main():
+ do_command(parse_options, sync)
diff --git a/RepSys/commands/up.py b/RepSys/commands/up.py
new file mode 100644
index 0000000..02a1a9f
--- /dev/null
+++ b/RepSys/commands/up.py
@@ -0,0 +1,22 @@
+from RepSys import Error
+from RepSys.command import *
+from RepSys.rpmutil import update
+
+HELP = """\
+Usage: repsys up [PATH]
+
+Update the package working copy and synchronize all binaries.
+
+Options:
+ -h help
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ if args:
+ opts.target = args[0]
+ return opts
+
+def main():
+ do_command(parse_options, update)
diff --git a/RepSys/commands/upload.py b/RepSys/commands/upload.py
new file mode 100644
index 0000000..6af50ea
--- /dev/null
+++ b/RepSys/commands/upload.py
@@ -0,0 +1,28 @@
+from RepSys import Error
+from RepSys.command import *
+from RepSys.rpmutil import upload
+
+HELP = """\
+Usage: repsys upload [OPTIONS] [PATH]
+
+Upload a given file to the binary sources repository.
+
+It will also update the contents of the 'binrepo.lst' file and leave it
+uncommited.
+
+If the path is a directory, all the contents of the directory will be
+uploaded or removed.
+
+Options:
+ -h help
+
+"""
+
+def parse_options():
+ parser = OptionParser(help=HELP)
+ opts, args = parser.parse_args()
+ opts.paths = args
+ return opts
+
+def main():
+ do_command(parse_options, upload)
diff --git a/RepSys/layout.py b/RepSys/layout.py
new file mode 100644
index 0000000..fb50acd
--- /dev/null
+++ b/RepSys/layout.py
@@ -0,0 +1,207 @@
+""" Handles repository layout scheme and package URLs."""
+
+import os
+import urlparse
+
+from RepSys import Error, config
+from RepSys.svn import SVN
+
+__all__ = ["package_url", "checkout_url", "repository_url", "get_url_revision"]
+
+def layout_dirs():
+ devel_branch = config.get("global", "trunk-dir", "cooker/")
+ devel_branch = os.path.normpath(devel_branch)
+ branches_dir = config.get("global", "branches-dir", "updates/")
+ branches_dir = os.path.normpath(branches_dir)
+ return devel_branch, branches_dir
+
+def get_url_revision(url, retrieve=True):
+ """Get the revision from a given URL
+
+ If the URL contains an explicit revision number (URL@REV), just use it
+ without even checking if the revision really exists.
+
+ The parameter retrieve defines whether it must ask the SVN server for
+ the revision number or not when it is not found in the URL.
+ """
+ url, rev = split_url_revision(url)
+ if rev is None and retrieve:
+ # if no revspec was found, ask the server
+ svn = SVN()
+ rev = svn.revision(url)
+ return rev
+
+def unsplit_url_revision(url, rev):
+ if rev is None:
+ newurl = url
+ else:
+ parsed = list(urlparse.urlparse(url))
+ path = os.path.normpath(parsed[2])
+ parsed[2] = path + "@" + str(rev)
+ newurl = urlparse.urlunparse(parsed)
+ return newurl
+
+def split_url_revision(url):
+ """Returns a tuple (url, rev) from an subversion URL with @REV
+
+ If the revision is not present in the URL, rev is None.
+ """
+ parsed = list(urlparse.urlparse(url))
+ path = os.path.normpath(parsed[2])
+ dirs = path.rsplit("/", 1)
+ lastname = dirs[-1]
+ newname = lastname
+ index = lastname.rfind("@")
+ rev = None
+ if index != -1:
+ newname = lastname[:index]
+ rawrev = lastname[index+1:]
+ if rawrev:
+ try:
+ rev = int(rawrev)
+ if rev < 0:
+ raise ValueError
+ except ValueError:
+ raise Error, "invalid revision specification on URL: %s" % url
+ dirs[-1] = newname
+ newpath = "/".join(dirs)
+ parsed[2] = newpath
+ newurl = urlparse.urlunparse(parsed)
+ return newurl, rev
+
+def checkout_url(pkgdirurl, branch=None, version=None, release=None,
+ releases=False, pristine=False, append_path=None):
+ """Get the URL of a branch of the package, defaults to current/
+
+ It tries to preserve revisions in the format @REV.
+ """
+ parsed = list(urlparse.urlparse(pkgdirurl))
+ path, rev = split_url_revision(parsed[2])
+ if releases:
+ path = os.path.normpath(path + "/releases")
+ elif version:
+ assert release is not None
+ path = os.path.normpath(path + "/releases/" + version + "/" + release)
+ elif pristine:
+ path = os.path.join(path, "pristine")
+ elif branch:
+ path = os.path.join(path, "branches", branch)
+ else:
+ path = os.path.join(path, "current")
+ if append_path:
+ path = os.path.join(path, append_path)
+ path = unsplit_url_revision(path, rev)
+ parsed[2] = path
+ newurl = urlparse.urlunparse(parsed)
+ return newurl
+
+def convert_default_parent(url):
+ """Removes the cooker/ component from the URL"""
+ parsed = list(urlparse.urlparse(url))
+ path = os.path.normpath(parsed[2])
+ rest, last = os.path.split(path)
+ parsed[2] = rest
+ newurl = urlparse.urlunparse(parsed)
+ return newurl
+
+def remove_current(pkgdirurl):
+ parsed = list(urlparse.urlparse(pkgdirurl))
+ path = os.path.normpath(parsed[2])
+ rest, last = os.path.split(path)
+ if last == "current":
+ # FIXME this way we will not allow packages to be named "current"
+ path = rest
+ parsed[2] = path
+ newurl = urlparse.urlunparse(parsed)
+ return newurl
+
+def repository_url(mirrored=False):
+ url = None
+ if mirrored and config.getbool("global", "use-mirror", "yes"):
+ url = config.get("global", "mirror")
+ if url is None:
+ url = config.get("global", "repository")
+ if not url:
+ # compatibility with the default_parent configuration option
+ default_parent = config.get("global", "default_parent")
+ if default_parent is None:
+ raise Error, "you need to set the 'repository' " \
+ "configuration option on repsys.conf"
+ url = convert_default_parent(default_parent)
+ return url
+
+def package_url(name_or_url, version=None, release=None, distro=None,
+ mirrored=True):
+ """Returns a tuple with the absolute package URL and its name
+
+ @name_or_url: name, relative path, or URL of the package. In case it is
+ a URL, the URL will just be 'normalized'.
+ @version: the version to be fetched from releases/ (requires release)
+ @release: the release number to be fetched from releases/$version/
+ @distro: the name of the repository branch inside updates/
+ @mirrored: return an URL based on the mirror repository, if enabled
+ """
+ from RepSys import mirror
+ if "://" in name_or_url:
+ pkgdirurl = mirror.normalize_path(name_or_url)
+ pkgdirurl = remove_current(pkgdirurl)
+ if mirror.using_on(pkgdirurl) and not mirrored:
+ pkgdirurl = mirror.relocate_path(mirror.mirror_url(),
+ repository_url(), pkgdirurl)
+ else:
+ name = name_or_url
+ devel_branch, branches_dir = layout_dirs()
+ if distro or "/" in name:
+ default_branch = branches_dir
+ if distro:
+ default_branch = os.path.join(default_branch, distro)
+ else:
+ default_branch = devel_branch # cooker
+ path = os.path.join(default_branch, name)
+ parsed = list(urlparse.urlparse(repository_url(mirrored=mirrored)))
+ parsed[2] = os.path.join(parsed[2], path)
+ pkgdirurl = urlparse.urlunparse(parsed)
+ return pkgdirurl
+
+def package_name(pkgdirurl):
+ """Returns the package name from a package URL
+
+ It takes care of revision numbers"""
+ parsed = urlparse.urlparse(pkgdirurl)
+ path, rev = split_url_revision(parsed[2])
+ rest, name = os.path.split(path)
+ return name
+
+def package_spec_url(pkgdirurl, *args, **kwargs):
+ """Returns the URL of the specfile of a given package URL
+
+ The parameters are the same used by checkout_url, except append_path.
+ """
+ kwargs["append_path"] = "SPECS/" + package_name(pkgdirurl) + ".spec"
+ specurl = checkout_url(pkgdirurl, *args, **kwargs)
+ return specurl
+
+def distro_branch(pkgdirurl):
+ """Tries to guess the distro branch name from a package URL"""
+ from RepSys.mirror import same_base
+ found = None
+ repo = repository_url()
+ if same_base(repo, pkgdirurl):
+ devel_branch, branches_dir = layout_dirs()
+ repo_path = urlparse.urlparse(repo)[2]
+ devel_path = os.path.join(repo_path, devel_branch)
+ branches_path = os.path.join(repo_path, branches_dir)
+ parsed = urlparse.urlparse(pkgdirurl)
+ path = os.path.normpath(parsed[2])
+ if path.startswith(devel_path):
+ # devel_branch must be before branches_dir in order to allow
+ # devel_branch to be inside branches_dir, as in /branches/cooker
+ _, found = os.path.split(devel_branch)
+ elif path.startswith(branches_path):
+ comps = path.split("/")
+ if branches_path == "/":
+ found = comps[1]
+ elif len(comps) >= 2: # must be at least branch/pkgname
+ found = comps[branches_path.count("/")+1]
+ return found
+
diff --git a/RepSys/log.py b/RepSys/log.py
new file mode 100644
index 0000000..6cb9da1
--- /dev/null
+++ b/RepSys/log.py
@@ -0,0 +1,633 @@
+#!/usr/bin/python
+from RepSys import Error, config, layout
+from RepSys.svn import SVN
+from RepSys.util import execcmd
+
+try:
+ from Cheetah.Template import Template
+except ImportError:
+ raise Error, "repsys requires the package python-cheetah"
+
+from cStringIO import StringIO
+
+import sys
+import os
+import re
+import time
+import locale
+import glob
+import tempfile
+import shutil
+import subprocess
+
+
+locale.setlocale(locale.LC_ALL, "C")
+
+default_template = """
+#if not $releases_by_author[-1].visible
+ ## Hide the first release that contains no changes. It must be a
+ ## reimported package and the log gathered from misc/ already should
+ ## contain a correct entry for the version-release:
+ #set $releases_by_author = $releases_by_author[:-1]
+#end if
+#for $rel in $releases_by_author
+* $rel.date $rel.author_name <$rel.author_email> $rel.version-$rel.release
++ Revision: $rel.revision
+## #if not $rel.released
+##+ Status: not released
+## #end if
+ #if not $rel.visible
++ rebuild (emptylog)
+ #end if
+ #for $rev in $rel.release_revisions
+ #for $line in $rev.lines
+$line
+ #end for
+ #end for
+
+ #for $author in $rel.authors
+ #if not $author.visible
+ #continue
+ #end if
+ ##alternatively, one could use:
+ ###if $author.email == "root"
+ ## #continue
+ ###end if
+ + $author.name <$author.email>
+ #for $rev in $author.revisions
+ #for $line in $rev.lines
+ $line
+ #end for
+ #end for
+
+ #end for
+#end for
+"""
+
+def getrelease(pkgdirurl, rev=None, macros=[], exported=None):
+ """Tries to obtain the version-release of the package for a
+ yet-not-markrelease revision of the package.
+
+ Is here where things should be changed if "automatic release increasing"
+ will be used.
+ """
+ from RepSys.rpmutil import rpm_macros_defs
+ svn = SVN()
+ pkgcurrenturl = os.path.join(pkgdirurl, "current")
+ specurl = os.path.join(pkgcurrenturl, "SPECS")
+ if exported is None:
+ tmpdir = tempfile.mktemp()
+ svn.export(specurl, tmpdir, rev=rev)
+ else:
+ tmpdir = os.path.join(exported, "SPECS")
+ try:
+ found = glob.glob(os.path.join(tmpdir, "*.spec"))
+ if not found:
+ raise Error, "no .spec file found inside %s" % specurl
+ specpath = found[0]
+ options = rpm_macros_defs(macros)
+ command = (("rpm -q --qf '%%{EPOCH}:%%{VERSION}-%%{RELEASE}\n' "
+ "--specfile %s %s") %
+ (specpath, options))
+ pipe = subprocess.Popen(command, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, shell=True)
+ pipe.wait()
+ output = pipe.stdout.read()
+ error = pipe.stderr.read()
+ if pipe.returncode != 0:
+ raise Error, "Error in command %s: %s" % (command, error)
+ releases = output.split()
+ try:
+ epoch, vr = releases[0].split(":", 1)
+ version, release = vr.split("-", 1)
+ except ValueError:
+ raise Error, "Invalid command output: %s: %s" % \
+ (command, output)
+ #XXX check if this is the right way:
+ if epoch == "(none)":
+ ev = version
+ else:
+ ev = epoch + ":" + version
+ return ev, release
+ finally:
+ if exported is None and os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+class _Revision:
+ lines = []
+ date = None
+ raw_date = None
+ revision = None
+ author_name = None
+ author_email = None
+
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def __repr__(self):
+ lines = repr(self.lines)[:30] + "...]"
+ line = "<_Revision %d author=%r date=%r lines=%s>" % \
+ (self.revision, self.author, self.date, lines)
+ return line
+
+
+class _Release(_Revision):
+ version = None
+ release = None
+ revisions = []
+ release_revisions = []
+ authors = []
+ visible = False
+
+ def __init__(self, **kwargs):
+ self.revisions = []
+ _Revision.__init__(self, **kwargs)
+
+ def __repr__(self):
+ line = "<_Release v=%s r=%s revs=%r>" % \
+ (self.version, self.release, self.revisions)
+ return line
+
+unescaped_macro_pat = re.compile(r"([^%])%([^%])")
+
+def escape_macros(text):
+ escaped = unescaped_macro_pat.sub("\\1%%\\2", text)
+ return escaped
+
+def format_lines(lines):
+ first = 1
+ entrylines = []
+ perexpr = re.compile(r"([^%])%([^%])")
+ for line in lines:
+ if line:
+ line = escape_macros(line)
+ if first:
+ first = 0
+ line = line.lstrip()
+ if line[0] != "-":
+ nextline = "- " + line
+ else:
+ nextline = line
+ elif line[0] != " " and line[0] != "-":
+ nextline = " " + line
+ else:
+ nextline = line
+ if nextline not in entrylines:
+ entrylines.append(nextline)
+ return entrylines
+
+
+class _Author:
+ name = None
+ email = None
+ revisions = None
+ visible = False
+
+
+def group_releases_by_author(releases):
+ allauthors = []
+ grouped = []
+ for release in releases:
+
+ # group revisions of the release by author
+ authors = {}
+ latest = None
+ for revision in release.revisions:
+ authors.setdefault(revision.author, []).append(revision)
+
+ # create _Authors and sort them by their latest revisions
+ decorated = []
+ for authorname, revs in authors.iteritems():
+ author = _Author()
+ author.name = revs[0].author_name
+ author.email = revs[0].author_email
+ author.revisions = revs
+ # #41117: mark those authors without visible messages
+ author.visible = bool(sum(len(rev.lines) for rev in revs))
+ revlatest = author.revisions[0]
+ # keep the latest revision even for completely invisible
+ # authors (below)
+ if latest is None or revlatest.revision > latest.revision:
+ latest = revlatest
+ if not author.visible:
+ # only sort those visible authors, invisible ones are used
+ # only in "latest"
+ continue
+ decorated.append((revlatest.revision, author))
+ decorated.sort(reverse=1)
+
+ if release.visible:
+ release.authors = [t[1] for t in decorated]
+ firstrel, release.authors = release.authors[0], release.authors[1:]
+ release.author_name = firstrel.name
+ release.author_email = firstrel.email
+ release.release_revisions = firstrel.revisions
+ else:
+ # we don't care about other possible authors in completely
+ # invisible releases
+ firstrev = release.revisions[0]
+ release.author_name = firstrev.author_name
+ release.author_email = firstrev.author_email
+ release.raw_date = firstrev.raw_date
+ release.date = firstrev.date
+
+ release.date = latest.date
+ release.raw_date = latest.raw_date
+ release.revision = latest.revision
+
+ grouped.append(release)
+
+ return grouped
+
+
+def group_revisions_by_author(currentlog):
+ revisions = []
+ last_author = None
+ for entry in currentlog:
+ revision = _Revision()
+ revision.lines = format_lines(entry.lines)
+ revision.raw_date = entry.date
+ revision.date = parse_raw_date(entry.date)
+ revision.revision = entry.revision
+ if entry.author == last_author:
+ revisions[-1].revisions.append(revision)
+ else:
+ author = _Author()
+ author.name, author.email = get_author_name(entry.author)
+ author.revisions = [revision]
+ revisions.append(author)
+ last_author = entry.author
+ return revisions
+
+
+emailpat = re.compile("(?P<name>.*?)\s*<(?P<email>.*?)>")
+
+def get_author_name(author):
+ found = emailpat.match(config.get("users", author, author))
+ name = ((found and found.group("name")) or author)
+ email = ((found and found.group("email")) or author)
+ return name, email
+
+def parse_raw_date(rawdate):
+ return time.strftime("%a %b %d %Y", rawdate)
+
+def filter_log_lines(lines):
+ # Lines in commit messages beginning with CLOG will be the only shown
+ # in the changelog. These lines will have the CLOG token and blanks
+ # stripped from the beginning.
+ onlylines = None
+ clogstr = config.get("log", "unignore-string")
+ if clogstr:
+ clogre = re.compile(r"(^%s[^ \t]?[ \t])" % clogstr)
+ onlylines = [clogre.sub("", line)
+ for line in lines if line.startswith(clogstr)]
+ if onlylines:
+ filtered = onlylines
+ else:
+ # Lines in commit messages containing SILENT at any position will be
+ # skipped; commits with their log messages beggining with SILENT in the
+ # first positionj of the first line will have all lines ignored.
+ ignstr = config.get("log", "ignore-string", "SILENT")
+ if len(lines) and lines[0].startswith(ignstr):
+ return []
+ filtered = [line for line in lines if ignstr not in line]
+ return filtered
+
+
+def make_release(author=None, revision=None, date=None, lines=None,
+ entries=[], released=True, version=None, release=None):
+ rel = _Release()
+ rel.author = author
+ if author:
+ rel.author_name, rel.author_email = get_author_name(author)
+ rel.revision = revision
+ rel.version = version
+ rel.release = release
+ rel.date = (date and parse_raw_date(date)) or None
+ rel.lines = lines
+ rel.released = released
+ rel.visible = False
+ for entry in entries:
+ lines = filter_log_lines(entry.lines)
+ revision = _Revision()
+ revision.revision = entry.revision
+ revision.lines = format_lines(lines)
+ if revision.lines:
+ rel.visible = True
+ revision.date = parse_raw_date(entry.date)
+ revision.raw_date = entry.date
+ revision.author = entry.author
+ (revision.author_name, revision.author_email) = \
+ get_author_name(entry.author)
+ rel.revisions.append(revision)
+ return rel
+
+
+def dump_file(releases, currentlog=None, template=None):
+ templpath = template or config.get("template", "path",
+ "/usr/share/repsys/default.chlog")
+ params = {}
+ if templpath is None or not os.path.exists(templpath):
+ params["source"] = default_template
+ sys.stderr.write("warning: %s not found. using built-in template.\n"%
+ templpath)
+ else:
+ params["file"] = templpath
+ releases_author = group_releases_by_author(releases)
+ revisions_author = group_revisions_by_author(currentlog)
+ params["searchList"] = [{"releases_by_author" : releases_author,
+ "releases" : releases,
+ "revisions_by_author": revisions_author}]
+ t = Template(**params)
+ return t.respond()
+
+
+class InvalidEntryError(Exception):
+ pass
+
+def parse_repsys_entry(revlog):
+ # parse entries in the format:
+ # %repsys <operation>
+ # key: value
+ # ..
+ # <newline>
+ # <comments>
+ #
+ if len(revlog.lines) == 0 or not revlog.lines[0].startswith("%repsys"):
+ raise InvalidEntryError
+ try:
+ data = {"operation" : revlog.lines[0].split()[1]}
+ except IndexError:
+ raise InvalidEntryError
+ for line in revlog.lines[1:]:
+ if not line:
+ break
+ try:
+ key, value = line.split(":", 1)
+ except ValueError:
+ raise InvalidEntryError
+ data[key.strip().lower()] = value.strip() # ???
+ return data
+
+
+def get_revision_offset():
+ try:
+ revoffset = config.getint("log", "revision-offset", 0)
+ except (ValueError, TypeError):
+ raise Error, ("Invalid revision-offset number in configuration "
+ "file(s).")
+ return revoffset or 0
+
+oldmsgpat = re.compile(
+ r"Copying release (?P<rel>[^\s]+) to (?P<dir>[^\s]+) directory\.")
+
+def parse_markrelease_log(relentry):
+ if not ((relentry.lines and oldmsgpat.match(relentry.lines[0]) \
+ or parse_repsys_entry(relentry))):
+ raise InvalidEntryError
+ from_rev = None
+ path = None
+ for changed in relentry.changed:
+ if changed["action"] == "A" and changed["from_rev"]:
+ from_rev = changed["from_rev"]
+ path = changed["path"]
+ break
+ else:
+ raise InvalidEntryError
+ # get the version and release from the names in the path, do not relay
+ # on log messages
+ version, release = path.rsplit(os.path.sep, 3)[-2:]
+ return version, release, from_rev
+
+
+def svn2rpm(pkgdirurl, rev=None, size=None, submit=False,
+ template=None, macros=[], exported=None):
+ concat = config.get("log", "concat", "").split()
+ revoffset = get_revision_offset()
+ svn = SVN()
+ pkgreleasesurl = layout.checkout_url(pkgdirurl, releases=True)
+ pkgcurrenturl = layout.checkout_url(pkgdirurl)
+ releaseslog = svn.log(pkgreleasesurl, noerror=1)
+ currentlog = svn.log(pkgcurrenturl, limit=size, start=rev,
+ end=revoffset)
+
+ # sort releases by copyfrom-revision, so that markreleases for same
+ # revisions won't look empty
+ releasesdata = []
+ if releaseslog:
+ for relentry in releaseslog[::-1]:
+ try:
+ (version, release, relrevision) = \
+ parse_markrelease_log(relentry)
+ except InvalidEntryError:
+ continue
+ releasesdata.append((relrevision, -relentry.revision, relentry,
+ version, release))
+ releasesdata.sort()
+
+ # collect valid releases using the versions provided by the changes and
+ # the packages
+ prevrevision = 0
+ releases = []
+ for (relrevision, dummy, relentry, version, release) in releasesdata:
+ if prevrevision == relrevision:
+ # ignore older markrelease of the same revision, since they
+ # will have no history
+ continue
+ entries = [entry for entry in currentlog
+ if relrevision >= entry.revision and
+ (prevrevision < entry.revision)]
+ if not entries:
+ #XXX probably a forced release, without commits in current/,
+ # check if this is the right behavior
+ sys.stderr.write("warning: skipping (possible) release "
+ "%s-%s@%s, no commits since previous markrelease (r%r)\n" %
+ (version, release, relrevision, prevrevision))
+ continue
+
+ release = make_release(author=relentry.author,
+ revision=relentry.revision, date=relentry.date,
+ lines=relentry.lines, entries=entries,
+ version=version, release=release)
+ releases.append(release)
+ prevrevision = relrevision
+
+ # look for commits that have been not submitted (released) yet
+ # this is done by getting all log entries newer (greater revision no.)
+ # than releasesdata[-1] (in the case it exists)
+ if releasesdata:
+ latest_revision = releasesdata[-1][0] # the latest copied rev
+ else:
+ latest_revision = 0
+ notsubmitted = [entry for entry in currentlog
+ if entry.revision > latest_revision]
+ if notsubmitted:
+ # if they are not submitted yet, what we have to do is to add
+ # a release/version number from getrelease()
+ version, release = getrelease(pkgdirurl, macros=macros,
+ exported=exported)
+ toprelease = make_release(entries=notsubmitted, released=False,
+ version=version, release=release)
+ releases.append(toprelease)
+
+ data = dump_file(releases[::-1], currentlog=currentlog, template=template)
+ return data
+
+def _split_changelog(stream):
+ current = None
+ count = 0
+ def finish(entry):
+ lines = entry[2]
+ # strip newlines at the end
+ for i in xrange(len(lines)-1, -1, -1):
+ if lines[i] != "\n":
+ break
+ del lines[i]
+ return entry
+ for line in stream:
+ if line.startswith("*"):
+ if current:
+ yield finish(current)
+ fields = line.split()
+ rawdate = " ".join(fields[:5])
+ try:
+ date = time.strptime(rawdate, "* %a %b %d %Y")
+ except ValueError, e:
+ raise Error, "failed to parse spec changelog: %s" % e
+ curlines = [line]
+ current = (date, count, curlines)
+ # count used to ensure stable sorting when changelog entries
+ # have the same date, otherwise it would also compare the
+ # changelog lines
+ count -= 1
+ elif current:
+ curlines.append(line)
+ else:
+ pass # not good, but ignore
+ if current:
+ yield finish(current)
+
+def sort_changelog(stream):
+ entries = _split_changelog(stream)
+ log = StringIO()
+ for time, count, elines in sorted(entries, reverse=True):
+ log.writelines(elines)
+ log.write("\n")
+ return log
+
+def split_spec_changelog(stream):
+ chlog = StringIO()
+ spec = StringIO()
+ found = 0
+ visible = 0
+ for line in stream:
+ if line.startswith("%changelog"):
+ found = 1
+ elif not found:
+ spec.write(line)
+ elif found:
+ if line.strip():
+ visible = 1
+ chlog.write(line)
+ elif line.startswith("%"):
+ found = 0
+ spec.write(line)
+ spec.seek(0)
+ if not visible:
+ # when there are only blanks in the changelog, make it empty
+ chlog = StringIO()
+ return spec, chlog
+
+def get_old_log(pkgdirurl):
+ chlog = StringIO()
+ oldurl = config.get("log", "oldurl")
+ if oldurl:
+ svn = SVN()
+ tmpdir = tempfile.mktemp()
+ try:
+ pkgname = layout.package_name(pkgdirurl)
+ pkgoldurl = os.path.join(oldurl, pkgname)
+ try:
+ # we're using HEAD here because fixes in misc/ (oldurl) may
+ # be newer than packages' last changed revision.
+ svn.export(pkgoldurl, tmpdir)
+ except Error:
+ pass
+ else:
+ logfile = os.path.join(tmpdir, "log")
+ if os.path.isfile(logfile):
+ file = open(logfile)
+ chlog.write("\n") # TODO needed?
+ log = file.read()
+ log = escape_macros(log)
+ chlog.write(log)
+ file.close()
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+ chlog.seek(0)
+ return chlog
+
+def get_changelog(pkgdirurl, another=None, svn=True, rev=None, size=None,
+ submit=False, sort=False, template=None, macros=[], exported=None,
+ oldlog=False):
+ """Generates the changelog for a given package URL
+
+ @another: a stream with the contents of a changelog to be merged with
+ the one generated
+ @svn: enable changelog from svn
+ @rev: generate the changelog with the changes up to the given
+ revision
+ @size: the number of revisions to be used (as in svn log --limit)
+ @submit: defines whether the latest unreleased log entries should have
+ the version parsed from the spec file
+ @sort: should changelog entries be reparsed and sorted after appending
+ the oldlog?
+ @template: the path to the cheetah template used to generate the
+ changelog from svn
+ @macros: a list of tuples containing macros to be defined when
+ parsing the version in the changelog
+ @exported: the path of a directory containing an already existing
+ checkout of the package, so that the spec file can be
+ parsed from there
+ @oldlog: if set it will try to append the old changelog file defined
+ in oldurl in repsys.conf
+ """
+ newlog = StringIO()
+ if svn:
+ rawsvnlog = svn2rpm(pkgdirurl, rev=rev, size=size, submit=submit,
+ template=template, macros=macros, exported=exported)
+ newlog.write(rawsvnlog)
+ if another:
+ newlog.writelines(another)
+ if oldlog:
+ newlog.writelines(get_old_log(pkgdirurl))
+ if sort:
+ newlog.seek(0)
+ newlog = sort_changelog(newlog)
+ newlog.seek(0)
+ return newlog
+
+def specfile_svn2rpm(pkgdirurl, specfile, rev=None, size=None,
+ submit=False, sort=False, template=None, macros=[], exported=None):
+ fi = open(specfile)
+ spec, oldchlog = split_spec_changelog(fi)
+ fi.close()
+ another = None
+ if config.getbool("log", "merge-spec", False):
+ another = oldchlog
+ sort = sort or config.getbool("log", "sort", False)
+ chlog = get_changelog(pkgdirurl, another=another, rev=rev, size=size,
+ submit=submit, sort=sort, template=template, macros=macros,
+ exported=exported, oldlog=True)
+ fo = open(specfile, "w")
+ fo.writelines(spec)
+ fo.write("\n\n%changelog\n")
+ fo.writelines(chlog)
+ fo.close()
+
+if __name__ == "__main__":
+ l = svn2rpm(sys.argv[1])
+ print l
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/mirror.py b/RepSys/mirror.py
new file mode 100644
index 0000000..94720cc
--- /dev/null
+++ b/RepSys/mirror.py
@@ -0,0 +1,129 @@
+import sys
+import os
+import urlparse
+import urllib
+
+from RepSys import Error, config, layout
+from RepSys.svn import SVN
+
+def mirror_url():
+ mirror = config.get("global", "mirror")
+ return mirror
+
+def normalize_path(url):
+ """normalize url for relocate_path needs"""
+ parsed = urlparse.urlparse(url)
+ path = os.path.normpath(parsed[2])
+ newurl = urlparse.urlunparse((parsed[0], parsed[1], path,
+ parsed[3], parsed[4], parsed[5]))
+ return newurl
+
+def _joinurl(url, relpath):
+ parsed = urlparse.urlparse(url)
+ newpath = os.path.join(parsed[2], relpath)
+ newurl = urlparse.urlunparse((parsed[0], parsed[1], newpath,
+ parsed[3], parsed[4], parsed[5]))
+ return newurl
+
+
+def strip_username(url):
+ parsed = list(urlparse.urlparse(url))
+ _, parsed[1] = urllib.splituser(parsed[1])
+ newurl = urlparse.urlunparse(parsed)
+ return newurl
+
+def same_base(parent, url):
+ """returns true if parent is parent of url"""
+ parent = normalize_path(parent)
+ url = normalize_path(url)
+ url = strip_username(url)
+ return url.startswith(parent)
+
+def relocate_path(oldparent, newparent, url):
+ oldparent = normalize_path(oldparent)
+ newparent = normalize_path(newparent)
+ url = normalize_path(url)
+ subpath = url[len(oldparent)+1:]
+ newurl = _joinurl(newparent, subpath) # subpath usually gets / at begining
+ return newurl
+
+def enabled(wcurl=None):
+ mirror = mirror_url()
+ repository = layout.repository_url()
+ enabled = False
+ if mirror and repository:
+ enabled = True
+ if wcurl and not same_base(mirror, wcurl):
+ enabled = False
+ return enabled
+
+def using_on(url):
+ """returnes True if the URL points to the mirror repository"""
+ mirror = mirror_url()
+ if mirror:
+ using = same_base(mirror, url)
+ else:
+ using = False
+ return using
+
+def info(url, write=False, stream=sys.stderr):
+ if using_on(url):
+ stream.write("Using the svn mirror.\n")
+ if write:
+ stream.write("To be able to commit changes, use "
+ "'repsys switch' first.\n")
+
+def mirror_relocate(oldparent, newparent, url, wcpath):
+ svn = SVN()
+ newurl = relocate_path(oldparent, newparent, url)
+ svn.switch(newurl, url, path=wcpath, relocate=True)
+ return newurl
+
+def switchto_parent(svn, url, path):
+ """Relocates the working copy to default_parent"""
+ newurl = mirror_relocate(mirror_url(), layout.repository_url(), url, path)
+ return newurl
+
+def switchto_parent_url(url):
+ newurl = relocate_path(mirror_url(), layout.repository_url(), url)
+ return newurl
+
+def switchto_mirror(svn, url, path):
+ newurl = mirror_relocate(layout.repository_url(), mirror_url(), url, path)
+ return newurl
+
+def autoswitch(svn, wcpath, wcurl, newbaseurl=None):
+ """Switches between mirror, default_parent, or newbaseurl"""
+ nobase = False
+ mirror = mirror_url()
+ repository = layout.repository_url()
+ current = repository
+ if repository is None:
+ raise Error, "the option repository from repsys.conf is "\
+ "required"
+ indefault = same_base(repository, wcurl)
+ if not newbaseurl:
+ if not mirror:
+ raise Error, "an URL is needed when the option mirror "\
+ "from repsys.conf is not set"
+ if indefault:
+ chosen = mirror
+ elif same_base(mirror, wcurl):
+ current = mirror
+ chosen = repository
+ else:
+ nobase = True
+ else:
+ if mirror and same_base(mirror, wcurl):
+ current = mirror
+ elif indefault:
+ pass # !!!!
+ else:
+ nobase = True
+ chosen = newbaseurl
+ if nobase:
+ raise Error, "the URL of this working copy is not based in "\
+ "repository nor mirror URLs"
+ assert current != chosen
+ newurl = mirror_relocate(current, chosen, wcurl, wcpath)
+ return newurl
diff --git a/RepSys/plugins/__init__.py b/RepSys/plugins/__init__.py
new file mode 100644
index 0000000..e4f4e08
--- /dev/null
+++ b/RepSys/plugins/__init__.py
@@ -0,0 +1,27 @@
+import os
+
+loaded = {}
+
+def load():
+ # based on smart's plugin system
+ pluginsdir = os.path.dirname(__file__)
+ for entry in os.listdir(pluginsdir):
+ if entry != "__init__.py" and entry.endswith(".py"):
+ name = entry[:-3]
+ loaded[name] = __import__("RepSys.plugins."+name, {}, {},
+ [name])
+ elif os.path.isdir(entry):
+ initfile = os.path.join(entry, "__init__.py")
+ if os.path.isfile(initfile):
+ loaded[entry] = __import__("RepSys.plugins."+entry, {}, {},
+ [entry])
+
+def list():
+ return loaded.keys()
+
+def help(name):
+ from RepSys import Error
+ try:
+ return loaded[name].__doc__
+ except KeyError:
+ raise Error, "plugin %s not found" % name
diff --git a/RepSys/plugins/ldapusers.py b/RepSys/plugins/ldapusers.py
new file mode 100644
index 0000000..e56371d
--- /dev/null
+++ b/RepSys/plugins/ldapusers.py
@@ -0,0 +1,189 @@
+"""
+A Repsys plugin for obtaining users from a LDAP server.
+
+In order to enable the plugin, the user must define the following
+options in the [global] section of repsys.conf:
+
+ ldap-uri [required if ldap-server is unset]
+ the URI of the server, you can refer to more than one server by
+ adding more URIs separated by spaces::
+
+ ldap-uri = ldap://ldap.network/ ldaps://backup.network:22389/
+
+ ldap-server [required if ldap-uri is unset]
+ the host name of the LDAP server
+ ldap-port [optional] [default: 389]
+ the port of the LDAP server
+ ldap-base [required]
+ the base DN where the search will be performed
+ ldap-binddn [optional] [default: empty]
+ the DN used to bind
+ ldap-bindpw [optional] [default: empty]
+ the password used to bind
+ ldap-starttls [optional] [default: no]
+ use "yes" or "no" to enable or disable the use of the STARTTLS
+ LDAP extension
+ ldap-filterformat [optional]
+ [default: (&(objectClass=inetOrgPerson)(uid=$username))]
+ RFC-2254 filter string used in the search of the user entry.
+ Note that this is a python template string and will have the
+ user name as parameter. For example:
+
+ ldap-filterformat = (&(objectClass=inetOrgPerson)(uid=$username))
+
+ Will result in the search filter:
+
+ (&(objectClass=inetOrgPerson)(uid=john))
+
+ ldap-resultformat [optional] [default: $cn <$mail>]
+ This is a python template string. This string will be
+ formatted using one dict object containing the fields
+ returned in the LDAP search, for example:
+
+ >>> format = Template("$cn <$mail>")
+ >>> d = search(basedn, filter)
+ >>> d
+ {"cn": "John Doe", "mail": "john@mandriva.org",
+ "uidNumber": "1290", "loginShell": "/bin/bash",
+ ... many other attributes ... }
+ >>> value = format.substitute(d)
+ >>> print value
+ John Doe <john@mandriva.org>
+
+ Note that only the first value of the attributes will be
+ used.
+
+When the searched option is not found, it will try in repsys.conf. All
+the values found. (including from repsys.conf) will be cached between
+each configuration access.
+
+This plugin requires the package python-ldap.
+
+For more information, look http://qa.mandriva.com/show_bug.cgi?id=30549
+"""
+from RepSys import Error, config
+
+import string
+
+users_cache = {}
+
+class LDAPError(Error):
+ def __init__(self, ldaperr):
+ self.ldaperr = ldaperr
+ name = ldaperr.__class__.__name__
+ desc = ldaperr.message["desc"]
+ self.message = "LDAP error %s: %s" % (name, desc)
+ self.args = self.message,
+
+def strip_entry(entry):
+ "Leave only the first value in all keys in the entry"
+ new = dict((key, value[0]) for key, value in entry.iteritems())
+ return new
+
+def interpolate(optname, format, data):
+ tmpl = string.Template(format)
+ try:
+ return tmpl.substitute(data)
+ except KeyError, e:
+ raise Error, "the key %s was not found in LDAP search, " \
+ "check your %s configuration" % (e, optname)
+ except (TypeError, ValueError), e:
+ raise Error, "LDAP response formatting error: %s. Check " \
+ "your %s configuration" % (e, optname)
+
+def used_attributes(format):
+ class DummyDict:
+ def __init__(self):
+ self.found = []
+ def __getitem__(self, key):
+ self.found.append(key)
+ return key
+ dd = DummyDict()
+ t = string.Template(format)
+ t.safe_substitute(dd)
+ return dd.found
+
+def make_handler():
+ uri = config.get("global", "ldap-uri")
+ if not uri:
+ server = config.get("global", "ldap-server")
+ if not server:
+ # ldap support is not enabled if ldap-uri nor ldap-server are
+ # defined
+ def dummy_wrapper(section, option=None, default=None, walk=False):
+ return config.get(section, option, default, wrap=False)
+ return dummy_wrapper
+
+ try:
+ port = int(config.get("global", "ldap-port", 389))
+ except ValueError:
+ raise Error, "the option ldap-port requires an integer, please "\
+ "check your configuration files"
+ uri = "ldap://%s:%d" % (server, port)
+
+ basedn = config.get("global", "ldap-base")
+ binddn = config.get("global", "ldap-binddn")
+ bindpw = config.get("global", "ldap-bindpw", "")
+ filterformat = config.get("global", "ldap-filterformat",
+ "(&(objectClass=inetOrgPerson)(uid=$username))", raw=1)
+ format = config.get("global", "ldap-resultformat", "$cn <$mail>", raw=1)
+
+ valid = {"yes": True, "no": False}
+ raw = config.get("global", "ldap-starttls", "no")
+ try:
+ starttls = valid[raw]
+ except KeyError:
+ raise Error, "invalid value %r for ldap-starttls, use "\
+ "'yes' or 'no'" % raw
+
+ try:
+ import ldap
+ except ImportError:
+ raise Error, "LDAP support needs the python-ldap package "\
+ "to be installed"
+ else:
+ from ldap.filter import escape_filter_chars
+
+ def users_wrapper(section, option=None, default=None, walk=False):
+ global users_cache
+ if walk:
+ raise Error, "ldapusers plugin does not support user listing"
+ assert option is not None, \
+ "When not section walking, option is required"
+
+ value = users_cache.get(option)
+ if value is not None:
+ return value
+
+ try:
+ l = ldap.initialize(uri)
+ if starttls:
+ l.start_tls_s()
+ if binddn:
+ l.bind(binddn, bindpw)
+ except ldap.LDAPError, e:
+ raise LDAPError(e)
+ try:
+ data = {"username": escape_filter_chars(option)}
+ filter = interpolate("ldap-filterformat", filterformat, data)
+ attrs = used_attributes(format)
+ try:
+ found = l.search_s(basedn, ldap.SCOPE_SUBTREE, filter,
+ attrlist=attrs)
+ except ldap.LDAPError, e:
+ raise LDAPError(e)
+ if found:
+ dn, entry = found[0]
+ entry = strip_entry(entry)
+ value = interpolate("ldap-resultformat", format, entry)
+ else:
+ # issue a warning?
+ value = config.get(section, option, default, wrap=False)
+ users_cache[option] = value
+ return value
+ finally:
+ l.unbind_s()
+
+ return users_wrapper
+
+config.wrap("users", handler=make_handler())
diff --git a/RepSys/plugins/sample.py.txt b/RepSys/plugins/sample.py.txt
new file mode 100644
index 0000000..9877f3c
--- /dev/null
+++ b/RepSys/plugins/sample.py.txt
@@ -0,0 +1,14 @@
+# Sample repsys plugin. In order to test it, rename to sample.py
+# vim:ft=python
+from RepSys import config
+
+def users_wrapper(section, option=None, default=None, walk=False):
+ d = {"foolano": "Foolano De Tal <foolano@bla.com>",
+ "ceeclano": "Ceeclano Algumacoisa <ceeclano@bli.com>",
+ "beltrano": "Beltrano Bla <beltrano@mail.ru>"}
+ if walk:
+ return d.items()
+
+ return d.get(option, default)
+
+config.wrap("users", handler=users_wrapper)
diff --git a/RepSys/rpmutil.py b/RepSys/rpmutil.py
new file mode 100644
index 0000000..bff744b
--- /dev/null
+++ b/RepSys/rpmutil.py
@@ -0,0 +1,759 @@
+#!/usr/bin/python
+from RepSys import Error, config
+from RepSys import mirror, layout, log, binrepo
+from RepSys.svn import SVN
+from RepSys.simplerpm import SRPM
+from RepSys.util import execcmd
+from RepSys.command import default_parent
+import rpm
+import urlparse
+import tempfile
+import shutil
+import string
+import glob
+import sys
+import os
+
+def get_spec(pkgdirurl, targetdir=".", submit=False):
+ svn = SVN()
+ tmpdir = tempfile.mktemp()
+ try:
+ geturl = layout.checkout_url(pkgdirurl, append_path="SPECS")
+ mirror.info(geturl)
+ svn.export("'%s'" % geturl, tmpdir)
+ speclist = glob.glob(os.path.join(tmpdir, "*.spec"))
+ if not speclist:
+ raise Error, "no spec files found"
+ spec = speclist[0]
+ shutil.copy(spec, targetdir)
+ name = os.path.basename(spec)
+ path = os.path.join(targetdir, name)
+ print "Wrote %s" % (name)
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+def rpm_macros_defs(macros):
+ defs = ("--define \"%s %s\"" % macro for macro in macros)
+ args = " ".join(defs)
+ return args
+
+#FIXME move it to another module
+def rev_touched_url(url, rev):
+ svn = SVN()
+ info = svn.info2(url)
+ if info is None:
+ raise Error, "can't fetch svn info about the URL: %s" % url
+ root = info["Repository Root"]
+ urlpath = url[len(root):]
+ touched = False
+ entries = svn.log(root, start=rev, limit=1)
+ entry = entries[0]
+ for change in entry.changed:
+ path = change.get("path")
+ if path and path.startswith(urlpath):
+ touched = True
+ return touched
+
+def get_srpm(pkgdirurl,
+ mode = "current",
+ targetdirs = None,
+ version = None,
+ release = None,
+ revision = None,
+ packager = "",
+ revname = 0,
+ svnlog = 0,
+ scripts = [],
+ submit = False,
+ template = None,
+ macros = [],
+ verbose = 0,
+ strict = False,
+ use_binrepo = False,
+ binrepo_check = True):
+ svn = SVN()
+ tmpdir = tempfile.mktemp()
+ topdir = "--define '_topdir %s'" % tmpdir
+ builddir = "--define '_builddir %s/%s'" % (tmpdir, "BUILD")
+ rpmdir = "--define '_rpmdir %s/%s'" % (tmpdir, "RPMS")
+ sourcedir = "--define '_sourcedir %s/%s'" % (tmpdir, "SOURCES")
+ specdir = "--define '_specdir %s/%s'" % (tmpdir, "SPECS")
+ srcrpmdir = "--define '_srcrpmdir %s/%s'" % (tmpdir, "SRPMS")
+ patchdir = "--define '_patchdir %s/%s'" % (tmpdir, "SOURCES")
+
+ try:
+ if mode == "version":
+ geturl = layout.checkout_url(pkgdirurl, version=version,
+ release=release)
+ elif mode == "pristine":
+ geturl = layout.checkout_url(pkgdirurl, pristine=True)
+ elif mode == "current" or mode == "revision":
+ #FIXME we should handle revisions specified using @REV
+ geturl = layout.checkout_url(pkgdirurl)
+ else:
+ raise Error, "unsupported get_srpm mode: %s" % mode
+ strict = strict or config.getbool("submit", "strict-revision", False)
+ if strict and not rev_touched_url(geturl, revision):
+ #FIXME would be nice to have the revision number even when
+ # revision is None
+ raise Error, "the revision %s does not change anything "\
+ "inside %s" % (revision or "HEAD", geturl)
+ mirror.info(geturl)
+ svn.export(geturl, tmpdir, rev=revision)
+ if use_binrepo:
+ binrepo_check = (binrepo_check or
+ config.getbool("binrepo", "getsrpm-check", False))
+ download_binaries(tmpdir, geturl, revision=revision,
+ export=True, check=binrepo_check)
+ srpmsdir = os.path.join(tmpdir, "SRPMS")
+ os.mkdir(srpmsdir)
+ specsdir = os.path.join(tmpdir, "SPECS")
+ speclist = glob.glob(os.path.join(specsdir, "*.spec"))
+ if config.getbool("srpm", "run-prep", False):
+ makefile = os.path.join(tmpdir, "Makefile")
+ if os.path.exists(makefile):
+ execcmd("make", "-C", tmpdir, "srpm-prep")
+ if not speclist:
+ raise Error, "no spec files found"
+ spec = speclist[0]
+ if svnlog:
+ submit = not not revision
+ log.specfile_svn2rpm(pkgdirurl, spec, revision, submit=submit,
+ template=template, macros=macros, exported=tmpdir)
+ for script in scripts:
+ #FIXME revision can be "None"
+ status, output = execcmd(script, tmpdir, spec, str(revision),
+ noerror=1)
+ if status != 0:
+ raise Error, "script %s failed" % script
+ if packager:
+ packager = " --define 'packager %s'" % packager
+
+ defs = rpm_macros_defs(macros)
+ sourcecmd = config.get("helper", "rpmbuild", "rpmbuild")
+ execcmd("%s -bs --nodeps %s %s %s %s %s %s %s %s %s %s" %
+ (sourcecmd, topdir, builddir, rpmdir, sourcedir, specdir,
+ srcrpmdir, patchdir, packager, spec, defs))
+
+ # copy the generated SRPMs to their target locations
+ targetsrpms = []
+ urlrev = None
+ if revname:
+ urlrev = revision or layout.get_url_revision(geturl)
+ if not targetdirs:
+ targetdirs = (".",)
+ srpms = glob.glob(os.path.join(srpmsdir, "*.src.rpm"))
+ if not srpms:
+ # something fishy happened
+ raise Error, "no SRPMS were found at %s" % srpmsdir
+ for srpm in srpms:
+ name = os.path.basename(srpm)
+ if revname:
+ name = "@%s:%s" % (urlrev, name)
+ for targetdir in targetdirs:
+ newpath = os.path.join(targetdir, name)
+ targetsrpms.append(newpath)
+ if os.path.exists(newpath):
+ # should we warn?
+ os.unlink(newpath)
+ shutil.copy(srpm, newpath)
+ if verbose:
+ sys.stderr.write("Wrote: %s\n" % newpath)
+ return targetsrpms
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+def patch_spec(pkgdirurl, patchfile, log=""):
+ #FIXME use get_spec
+ svn = SVN()
+ tmpdir = tempfile.mktemp()
+ try:
+ geturl = layout.checkout_url(pkgdirurl, append_path="SPECS")
+ svn.checkout(geturl, tmpdir)
+ speclist = glob.glob(os.path.join(tmpdir, "*.spec"))
+ if not speclist:
+ raise Error, "no spec files found"
+ spec = speclist[0]
+ status, output = execcmd("patch", spec, patchfile)
+ if status != 0:
+ raise Error, "can't apply patch:\n%s\n" % output
+ else:
+ svn.commit(tmpdir, log="")
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+def put_srpm(srpmfile, markrelease=False, striplog=True, branch=None,
+ baseurl=None, baseold=None, logmsg=None, rename=True):
+ svn = SVN()
+ srpm = SRPM(srpmfile)
+ tmpdir = tempfile.mktemp()
+ if baseurl:
+ pkgurl = mirror._joinurl(baseurl, srpm.name)
+ else:
+ pkgurl = layout.package_url(srpm.name, distro=branch,
+ mirrored=False)
+ print "Importing package to %s" % pkgurl
+ try:
+ if srpm.epoch:
+ version = "%s:%s" % (srpm.epoch, srpm.version)
+ else:
+ version = srpm.version
+ versionurl = "/".join([pkgurl, "releases", version])
+ releaseurl = "/".join([versionurl, srpm.release])
+ currenturl = "/".join([pkgurl, "current"])
+ currentdir = os.path.join(tmpdir, "current")
+ #FIXME when pre-commit hook fails, there's no clear way to know
+ # what happened
+ ret = svn.mkdir(pkgurl, noerror=1, log="Created package directory")
+ if ret or not svn.ls(currenturl, noerror=1):
+ svn.checkout(pkgurl, tmpdir)
+ svn.mkdir(os.path.join(tmpdir, "releases"))
+ svn.mkdir(currentdir)
+ svn.mkdir(os.path.join(currentdir, "SPECS"))
+ svn.mkdir(os.path.join(currentdir, "SOURCES"))
+ #svn.commit(tmpdir,log="Created package structure.")
+ version_exists = 1
+ else:
+ if svn.ls(releaseurl, noerror=1):
+ raise Error, "release already exists"
+ svn.checkout("/".join([pkgurl, "current"]), tmpdir)
+ svn.mkdir(versionurl, noerror=1,
+ log="Created directory for version %s." % version)
+ currentdir = tmpdir
+
+ specsdir = os.path.join(currentdir, "SPECS")
+ sourcesdir = os.path.join(currentdir, "SOURCES")
+
+ unpackdir = tempfile.mktemp()
+ os.mkdir(unpackdir)
+ try:
+ srpm.unpack(unpackdir)
+
+ uspecsdir = os.path.join(unpackdir, "SPECS")
+ usourcesdir = os.path.join(unpackdir, "SOURCES")
+
+ uspecsentries = os.listdir(uspecsdir)
+ usourcesentries = os.listdir(usourcesdir)
+ specsentries = os.listdir(specsdir)
+ sourcesentries = os.listdir(sourcesdir)
+
+ # Remove old entries
+ for entry in [x for x in specsentries
+ if x not in uspecsentries]:
+ if entry == ".svn":
+ continue
+ entrypath = os.path.join(specsdir, entry)
+ os.unlink(entrypath)
+ svn.remove(entrypath)
+ for entry in [x for x in sourcesentries
+ if x not in usourcesentries]:
+ if entry == ".svn":
+ continue
+ entrypath = os.path.join(sourcesdir, entry)
+ os.unlink(entrypath)
+ svn.remove(entrypath)
+
+ # Copy all files
+ execcmd("cp -rf", uspecsdir, currentdir)
+ execcmd("cp -rf", usourcesdir, currentdir)
+
+ # Add new entries
+ for entry in [x for x in uspecsentries
+ if x not in specsentries]:
+ entrypath = os.path.join(specsdir, entry)
+ svn.add(entrypath)
+ for entry in [x for x in usourcesentries
+ if x not in sourcesentries]:
+ entrypath = os.path.join(sourcesdir, entry)
+ svn.add(entrypath)
+ finally:
+ if os.path.isdir(unpackdir):
+ shutil.rmtree(unpackdir)
+
+ specs = glob.glob(os.path.join(specsdir, "*.spec"))
+ if not specs:
+ raise Error, "no spec file found on %s" % specsdir
+ if len(specs) > 1:
+ raise Error, "more than one spec file found on %s" % specsdir
+ specpath = specs[0]
+ if rename:
+ specfile = os.path.basename(specpath)
+ specname = specfile[:-len(".spec")]
+ if specname != srpm.name:
+ newname = srpm.name + ".spec"
+ newpath = os.path.join(specsdir, newname)
+ sys.stderr.write("warning: renaming spec file to '%s' "
+ "(use -n to disable it)\n" % (newname))
+ os.rename(specpath, newpath)
+ try:
+ svn.remove(specpath)
+ except Error:
+ # file not tracked
+ svn.revert(specpath)
+ svn.add(newpath)
+ specpath = newpath
+
+ if striplog:
+ specpath = specpath
+ fspec = open(specpath)
+ spec, chlog = log.split_spec_changelog(fspec)
+ fspec.close()
+ fspec = open(specpath, "w")
+ fspec.writelines(spec)
+ fspec.close()
+ chlog.seek(0, os.SEEK_END)
+ if chlog.tell() != 0:
+ chlog.seek(0)
+ #FIXME move it to layout.py
+ oldurl = baseold or config.get("log", "oldurl")
+ pkgoldurl = mirror._joinurl(oldurl, srpm.name)
+ svn.mkdir(pkgoldurl, noerror=1,
+ log="created old log directory for %s" % srpm.name)
+ logtmp = tempfile.mktemp()
+ try:
+ svn.checkout(pkgoldurl, logtmp)
+ miscpath = os.path.join(logtmp, "log")
+ fmisc = open(miscpath, "w+")
+ fmisc.writelines(chlog)
+ fmisc.close()
+ svn.add(miscpath)
+ svn.commit(logtmp,
+ log="imported old log for %s" % srpm.name)
+ finally:
+ if os.path.isdir(logtmp):
+ shutil.rmtree(logtmp)
+ binrepo.import_binaries(currentdir, srpm.name)
+ svn.commit(tmpdir,
+ log=logmsg or ("imported package %s" % srpm.name))
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+ # Do revision and pristine tag copies
+ pristineurl = layout.checkout_url(pkgurl, pristine=True)
+ svn.remove(pristineurl, noerror=1,
+ log="Removing previous pristine/ directory.")
+ currenturl = layout.checkout_url(pkgurl)
+ svn.copy(currenturl, pristineurl,
+ log="Copying release %s-%s to pristine/ directory." %
+ (version, srpm.release))
+ if markrelease:
+ svn.copy(currenturl, releaseurl,
+ log="Copying release %s-%s to releases/ directory." %
+ (version, srpm.release))
+
+def create_package(pkgdirurl, log="", verbose=0):
+ svn = SVN()
+ tmpdir = tempfile.mktemp()
+ try:
+ basename = layout.package_name(pkgdirurl)
+ if verbose:
+ print "Creating package directory...",
+ sys.stdout.flush()
+ ret = svn.mkdir(pkgdirurl,
+ log="Created package directory for '%s'." % basename)
+ if verbose:
+ print "done"
+ print "Checking it out...",
+ svn.checkout(pkgdirurl, tmpdir)
+ if verbose:
+ print "done"
+ print "Creating package structure...",
+ svn.mkdir(os.path.join(tmpdir, "current"))
+ svn.mkdir(os.path.join(tmpdir, "current", "SPECS"))
+ svn.mkdir(os.path.join(tmpdir, "current", "SOURCES"))
+ if verbose:
+ print "done"
+ print "Committing...",
+ svn.commit(tmpdir,
+ log="Created package structure for '%s'." % basename)
+ print "done"
+ finally:
+ if os.path.isdir(tmpdir):
+ shutil.rmtree(tmpdir)
+
+
+def create_markrelease_log(version, release, revision):
+ log = """%%repsys markrelease
+version: %s
+release: %s
+revision: %s
+
+%s""" % (version, release, revision,
+ ("Copying %s-%s to releases/ directory." % (version, release)))
+ return log
+
+def mark_release(pkgdirurl, version, release, revision):
+ svn = SVN()
+ releasesurl = layout.checkout_url(pkgdirurl, releases=True)
+ versionurl = "/".join([releasesurl, version])
+ releaseurl = "/".join([versionurl, release])
+ currenturl = layout.checkout_url(pkgdirurl)
+ binrepo.markrelease(currenturl, releasesurl, version, release, revision)
+ if svn.ls(releaseurl, noerror=1):
+ raise Error, "release already exists"
+ svn.mkdir(releasesurl, noerror=1,
+ log="Created releases directory.")
+ svn.mkdir(versionurl, noerror=1,
+ log="Created directory for version %s." % version)
+ pristineurl = layout.checkout_url(pkgdirurl, pristine=True)
+ svn.remove(pristineurl, noerror=1,
+ log="Removing previous pristine/ directory.")
+ svn.copy(currenturl, pristineurl,
+ log="Copying release %s-%s to pristine/ directory." %
+ (version, release))
+ markreleaselog = create_markrelease_log(version, release, revision)
+ svn.copy(currenturl, releaseurl, rev=revision,
+ log=markreleaselog)
+
+def check_changed(pkgdirurl, all=0, show=0, verbose=0):
+ svn = SVN()
+ if all:
+ baseurl = pkgdirurl
+ packages = []
+ if verbose:
+ print "Getting list of packages...",
+ sys.stdout.flush()
+ packages = [x[:-1] for x in svn.ls(baseurl)]
+ if verbose:
+ print "done"
+ if not packages:
+ raise Error, "couldn't get list of packages"
+ else:
+ baseurl, basename = os.path.split(pkgdirurl)
+ packages = [basename]
+ clean = []
+ changed = []
+ nopristine = []
+ nocurrent = []
+ for package in packages:
+ pkgdirurl = os.path.join(baseurl, package)
+ current = layout.checkout_url(pkgdirurl)
+ pristine = layout.checkout_url(pkgdirurl, pristine=True)
+ if verbose:
+ print "Checking package %s..." % package,
+ sys.stdout.flush()
+ if not svn.ls(current, noerror=1):
+ if verbose:
+ print "NO CURRENT"
+ nocurrent.append(package)
+ elif not svn.ls(pristine, noerror=1):
+ if verbose:
+ print "NO PRISTINE"
+ nopristine.append(package)
+ else:
+ diff = svn.diff(pristine, current)
+ if diff:
+ changed.append(package)
+ if verbose:
+ print "CHANGED"
+ if show:
+ print diff
+ else:
+ if verbose:
+ print "clean"
+ clean.append(package)
+ if verbose:
+ if not packages:
+ print "No packages found!"
+ elif all:
+ print "Total clean packages: %s" % len(clean)
+ print "Total CHANGED packages: %d" % len(changed)
+ print "Total NO CURRENT packages: %s" % len(nocurrent)
+ print "Total NO PRISTINE packages: %s" % len(nopristine)
+ return {"clean": clean,
+ "changed": changed,
+ "nocurrent": nocurrent,
+ "nopristine": nopristine}
+
+def checkout(pkgdirurl, path=None, revision=None, branch=None, distro=None,
+ spec=False, use_binrepo=False, binrepo_check=True, binrepo_link=True):
+ o_pkgdirurl = pkgdirurl
+ pkgdirurl = layout.package_url(o_pkgdirurl, distro=distro)
+ append = None
+ if spec:
+ append = "SPECS"
+ current = layout.checkout_url(pkgdirurl, branch=branch,
+ append_path=append)
+ if path is None:
+ path = layout.package_name(pkgdirurl)
+ mirror.info(current, write=True)
+ svn = SVN()
+ svn.checkout(current, path, rev=revision, show=1)
+ if use_binrepo:
+ download_binaries(path, revision=revision, symlinks=binrepo_link,
+ check=binrepo_check)
+
+def getpkgtopdir(basedir=None):
+ #FIXME this implementation doesn't work well with relative path names,
+ # which is something we need in order to have a friendlier output
+ if basedir is None:
+ basedir = os.path.curdir
+ while not ispkgtopdir(basedir):
+ if os.path.abspath(basedir) == "/":
+ raise Error, "can't find top package directories SOURCES and SPECS"
+ basedir = os.path.join(basedir, os.path.pardir)
+ if basedir.startswith("./"):
+ basedir = basedir[2:]
+ return basedir
+
+def ispkgtopdir(path=None):
+ if path is None:
+ path = os.getcwd()
+ names = os.listdir(path)
+ return (".svn" in names and "SPECS" in names and "SOURCES" in names)
+
+def sync(dryrun=False, ci=False, download=False):
+ # TODO FIXME XXX fix it!
+ raise Error, "sync is not expected to work these days"
+ svn = SVN()
+ topdir = getpkgtopdir()
+ # run svn info because svn st does not complain when topdir is not an
+ # working copy
+ svn.info(topdir)
+ specsdir = os.path.join(topdir, "SPECS/")
+ sourcesdir = os.path.join(topdir, "SOURCES/")
+ for path in (specsdir, sourcesdir):
+ if not os.path.isdir(path):
+ raise Error, "%s directory not found" % path
+ specs = glob.glob(os.path.join(specsdir, "*.spec"))
+ if not specs:
+ raise Error, "no .spec files found in %s" % specsdir
+ specpath = specs[0] # FIXME better way?
+ try:
+ rpm.addMacro("_topdir", os.path.abspath(topdir))
+ spec = rpm.TransactionSet().parseSpec(specpath)
+ except rpm.error, e:
+ raise Error, "could not load spec file: %s" % e
+ sources = dict((os.path.basename(name), name)
+ for name, no, flags in spec.sources())
+ sourcesst = dict((os.path.basename(path), (path, st))
+ for st, path in svn.status(sourcesdir, noignore=True))
+ toadd_br = []
+ toadd_svn = []
+ toremove_svn = []
+ toremove_br = []
+ # add the spec file itself, in case of a new package
+ specstl = svn.status(specpath, noignore=True)
+ if specstl:
+ specst, _ = specstl[0]
+ if specst == "?":
+ toadd_svn.append(specpath)
+ # add source files:
+ for source, url in sources.iteritems():
+ sourcepath = os.path.join(sourcesdir, source)
+ if sourcesst.get(source):
+ if not os.path.islink(sourcepath):
+ if not binrepo.is_tracked(sourcepath):
+ if binrepo.is_binary(sourcepath):
+ toadd_br.append(sourcepath)
+ else:
+ toadd_svn.append(sourcepath)
+ else:
+ sys.stderr.write("warning: %s not found\n" % sourcepath)
+ elif download and not os.path.isfile(sourcepath):
+ print "%s not found, downloading from %s" % (sourcepath, url)
+ fmt = config.get("global", "download-command",
+ "wget -c -O '$dest' $url")
+ context = {"dest": sourcepath, "url": url}
+ try:
+ cmd = string.Template(fmt).substitute(context)
+ except KeyError, e:
+ raise Error, "invalid variable %r in download-command "\
+ "configuration option" % e
+ execcmd(cmd, show=True)
+ if os.path.isfile(sourcepath):
+ if binrepo.is_binary(sourcepath):
+ toadd_br.append(sourcepath)
+ else:
+ toadd_svn.append(sourcepath)
+ else:
+ raise Error, "file not found: %s" % sourcepath
+ # rm entries not found in sources and still in svn
+ found = os.listdir(sourcesdir)
+ for entry in found:
+ if entry == ".svn" or entry == "sources":
+ continue
+ status = sourcesst.get(entry)
+ path = os.path.join(sourcesdir, entry)
+ if entry not in sources:
+ if status is None: # file is tracked by svn
+ toremove_svn.append(path)
+ elif binrepo.is_tracked(path):
+ toremove_br.append(path)
+ for path in toremove_svn:
+ print "D\t%s" % path
+ if not dryrun:
+ svn.remove(path, local=True)
+ for path in toremove_br:
+ print "DB\t%s" % path
+ if not dryrun:
+ binrepo.delete_pending(path)
+ for path in toadd_svn:
+ print "A\t%s" % path
+ if not dryrun:
+ svn.add(path, local=True)
+ for path in toadd_br:
+ print "AB\t%s" % path
+ if not dryrun:
+ binrepo.upload_pending(path)
+ if commit:
+ commit(topdir)
+
+def commit(target=".", message=None, logfile=None):
+ topdir = getpkgtopdir(target)
+ sourcesdir = os.path.join(topdir, "SOURCES")
+ binrepo.commit(sourcesdir) #TODO make it optional
+ svn = SVN()
+ status = svn.status(target, quiet=True)
+ if not status:
+ print "nothing to commit"
+ return
+ info = svn.info2(target)
+ url = info.get("URL")
+ if url is None:
+ raise Error, "working copy URL not provided by svn info"
+ mirrored = mirror.using_on(url)
+ if mirrored:
+ newurl = mirror.switchto_parent(svn, url, target)
+ print "relocated to", newurl
+ # we can't use the svn object here because svn --non-interactive option
+ # hides VISUAL
+ opts = []
+ if message is not None:
+ opts.append("-m \"%s\"" % message)
+ if logfile is not None:
+ opts.append("-F \"%s\"" % logfile)
+ mopts = " ".join(opts)
+ os.system("svn ci %s %s" % (mopts, target))
+ if mirrored:
+ print "use \"repsys switch\" in order to switch back to mirror "\
+ "later"
+
+def spec_sources(topdir):
+ specs = glob.glob(os.path.join(topdir, "SPECS/*.spec"))
+ spec_path = specs[0] # FIXME use svn info to ensure which one
+ ts = rpm.ts()
+ spec = ts.parseSpec(spec_path)
+ sources = [name for name, x, y in spec.sources()]
+ return sources
+
+def download_binaries(target, pkgdirurl=None, export=False, revision=None,
+ symlinks=True, check=False):
+ refurl = pkgdirurl
+ if refurl is None:
+ refurl = binrepo.svn_root(target)
+ if binrepo.enabled(refurl):
+ binrepo.download(target, pkgdirurl, export=export,
+ revision=revision, symlinks=symlinks, check=check)
+
+def update(target=None):
+ svn = SVN()
+ info = None
+ svn_target = None
+ br_target = None
+ if target:
+ svn_target = target
+ else:
+ top = getpkgtopdir()
+ svn_target = top
+ br_target = top
+ if svn_target:
+ svn.update(svn_target, show=True)
+ if br_target:
+ info = svn.info2(svn_target)
+ if not br_target and not svn_target:
+ raise Error, "target not in SVN nor in binaries "\
+ "repository: %s" % target
+ url = info["URL"]
+ download_binaries(br_target, url)
+
+def upload(paths):
+ for path in paths:
+ binrepo.upload(path)
+
+def binrepo_delete(paths, commit=False):
+ #TODO handle files tracked by svn
+ refurl = binrepo.svn_root(paths[0])
+ if not binrepo.enabled(refurl):
+ raise Error, "binary repository is not enabled for %s" % refurl
+ added, deleted = binrepo.remove(paths)
+ if commit:
+ svn = SVN()
+ spath = binrepo.sources_path(paths[0])
+ log = _sources_log(added, deleted)
+ svn.commit(spath, log=log)
+
+def switch(mirrorurl=None):
+ svn = SVN()
+ topdir = getpkgtopdir()
+ info = svn.info2(topdir)
+ wcurl = info.get("URL")
+ if wcurl is None:
+ raise Error, "working copy URL not provided by svn info"
+ newurl = mirror.autoswitch(svn, topdir, wcurl, mirrorurl)
+ print "switched to", newurl
+
+def get_submit_info(path):
+ path = os.path.abspath(path)
+
+ # First, look for SPECS and SOURCES directories.
+ found = False
+ while path != "/":
+ if os.path.isdir(path):
+ specsdir = os.path.join(path, "SPECS")
+ sourcesdir = os.path.join(path, "SOURCES")
+ if os.path.isdir(specsdir) and os.path.isdir(sourcesdir):
+ found = True
+ break
+ path = os.path.dirname(path)
+ if not found:
+ raise Error, "SPECS and/or SOURCES directories not found"
+
+ # Then, check if this is really a subversion directory.
+ if not os.path.isdir(os.path.join(path, ".svn")):
+ raise Error, "subversion directory not found"
+
+ svn = SVN()
+
+ # Now, extract the package name.
+ info = svn.info2(path)
+ url = info.get("URL")
+ if url is None:
+ raise Error, "missing URL from svn info %s" % path
+ toks = url.split("/")
+ if len(toks) < 2 or toks[-1] != "current":
+ raise Error, "unexpected URL received from 'svn info'"
+ name = toks[-2]
+ url = "/".join(toks[:-1])
+
+ # Finally, guess revision.
+ max = -1
+ files = []
+ files.extend(glob.glob("%s/*" % specsdir))
+ files.extend(glob.glob("%s/*" % sourcesdir))
+ for file in files:
+ try:
+ info = svn.info2(file)
+ except Error:
+ # possibly not tracked
+ continue
+ if info is None:
+ continue
+ rawrev = info.get("Last Changed Rev")
+ if rawrev:
+ rev = int(rawrev)
+ if rev > max:
+ max = rev
+ if max == -1:
+ raise Error, "revision tag not found in 'svn info' output"
+
+ if mirror.using_on(url):
+ url = mirror.switchto_parent_url(url)
+
+ return name, url, max
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/simplerpm.py b/RepSys/simplerpm.py
new file mode 100644
index 0000000..d448c5f
--- /dev/null
+++ b/RepSys/simplerpm.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+from RepSys.util import execcmd
+
+class SRPM:
+ def __init__(self, filename):
+ self.filename = filename
+ self._getinfo()
+
+ def _getinfo(self):
+ cmdstr = "rpm -qp --qf '%%{name} %%{epoch} %%{release} %%{version}' %s"
+ status, output = execcmd(cmdstr % self.filename)
+ self.name, self.epoch, self.release, self.version = output.split()
+ if self.epoch == "(none)":
+ self.epoch = None
+
+ def unpack(self, topdir):
+ execcmd("rpm -i --define '_topdir %s' %s" % (topdir, self.filename))
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/svn.py b/RepSys/svn.py
new file mode 100644
index 0000000..d6be524
--- /dev/null
+++ b/RepSys/svn.py
@@ -0,0 +1,430 @@
+from RepSys import Error, SilentError, config
+from RepSys.util import execcmd, get_auth
+import sys
+import os
+import re
+import time
+
+__all__ = ["SVN", "SVNLook", "SVNLogEntry"]
+
+class SVNLogEntry:
+ def __init__(self, revision, author, date):
+ self.revision = revision
+ self.author = author
+ self.date = date
+ self.changed = []
+ self.lines = []
+
+ def __cmp__(self, other):
+ return cmp(self.date, other.date)
+
+class SVN:
+ def _execsvn(self, *args, **kwargs):
+ localcmds = ("add", "revert", "cleanup")
+ if not kwargs.get("show") and args[0] not in localcmds:
+ args = list(args)
+ args.append("--non-interactive")
+ else:
+ kwargs["geterr"] = True
+ kwargs["cleanerr"] = True
+ if kwargs.get("xml"):
+ args.append("--xml")
+ self._set_env()
+ svn_command = config.get("global", "svn-command", "svn")
+ cmdstr = svn_command + " " + " ".join(args)
+ try:
+ return execcmd(cmdstr, **kwargs)
+ except Error, e:
+ msg = None
+ if e.args:
+ if "Permission denied" in e.args[0]:
+ msg = ("It seems ssh-agent or ForwardAgent are not setup "
+ "or your username is wrong. See "
+ "http://wiki.mandriva.com/en/Development/Docs/Contributor_Tricks#SSH_configuration"
+ " for more information.")
+ elif "authorization failed" in e.args[0]:
+ msg = ("Note that repsys does not support any HTTP "
+ "authenticated access.")
+ if kwargs.get("show") and \
+ not config.getbool("global", "verbose", 0):
+ # svn has already dumped error messages, we don't need to
+ # do it too
+ if msg:
+ sys.stderr.write("\n")
+ sys.stderr.write(msg)
+ sys.stderr.write("\n")
+ raise SilentError
+ elif msg:
+ raise Error, "%s\n%s" % (e, msg)
+ raise
+
+ def _set_env(self):
+ wrapper = "repsys-ssh"
+ repsys = config.get("global", "repsys-cmd")
+ if repsys:
+ dir = os.path.dirname(repsys)
+ path = os.path.join(dir, wrapper)
+ if os.path.exists(path):
+ wrapper = path
+ defaults = {"SVN_SSH": wrapper}
+ os.environ.update(defaults)
+ raw = config.get("global", "svn-env")
+ if raw:
+ for line in raw.split("\n"):
+ env = line.strip()
+ if not env:
+ continue
+ try:
+ name, value = env.split("=", 1)
+ except ValueError:
+ sys.stderr.write("invalid svn environment line: %r\n" % env)
+ continue
+ os.environ[name] = value
+
+ def _execsvn_success(self, *args, **kwargs):
+ status, output = self._execsvn(*args, **kwargs)
+ return status == 0
+
+ def _add_log(self, cmd_args, received_kwargs, optional=0):
+ if (not optional or
+ received_kwargs.has_key("log") or
+ received_kwargs.has_key("logfile")):
+ ret = received_kwargs.get("log")
+ if ret is not None:
+ cmd_args.append("-m '%s'" % ret)
+ ret = received_kwargs.get("logfile")
+ if ret is not None:
+ cmd_args.append("-F '%s'" % ret)
+
+ def _add_revision(self, cmd_args, received_kwargs, optional=0):
+ if not optional or received_kwargs.has_key("rev"):
+ ret = received_kwargs.get("rev")
+ if isinstance(ret, basestring):
+ if not ret.startswith("{"): # if not a datespec
+ try:
+ ret = int(ret)
+ except ValueError:
+ raise Error, "invalid revision provided"
+ if ret:
+ cmd_args.append("-r '%s'" % ret)
+
+ def add(self, path, **kwargs):
+ cmd = ["add", path]
+ return self._execsvn_success(noauth=1, *cmd, **kwargs)
+
+ def copy(self, pathfrom, pathto, **kwargs):
+ cmd = ["copy", pathfrom, pathto]
+ self._add_revision(cmd, kwargs, optional=1)
+ self._add_log(cmd, kwargs)
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def remove(self, path, force=0, **kwargs):
+ cmd = ["remove", path]
+ self._add_log(cmd, kwargs)
+ if force:
+ cmd.append("--force")
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def mkdir(self, path, **kwargs):
+ cmd = ["mkdir", path]
+ if kwargs.get("parents"):
+ cmd.append("--parents")
+ self._add_log(cmd, kwargs)
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def _execsvn_commit(self, *cmd, **kwargs):
+ status, output = self._execsvn(*cmd, **kwargs)
+ match = re.search("Committed revision (?P<rev>\\d+)\\.$", output)
+ if match:
+ rawrev = match.group("rev")
+ return int(rawrev)
+
+ def commit(self, path, **kwargs):
+ cmd = ["commit", path]
+ if kwargs.get("nonrecursive"):
+ cmd.append("-N")
+ self._add_log(cmd, kwargs)
+ return self._execsvn_commit(*cmd, **kwargs)
+
+ def import_(self, path, url, **kwargs):
+ cmd = ["import", "'%s'" % path, "'%s'" % url]
+ self._add_log(cmd, kwargs)
+ return self._execsvn_commit(*cmd, **kwargs)
+
+ def export(self, url, targetpath, **kwargs):
+ cmd = ["export", "'%s'" % url, targetpath]
+ self._add_revision(cmd, kwargs, optional=1)
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def checkout(self, url, targetpath, **kwargs):
+ cmd = ["checkout", "'%s'" % url, targetpath]
+ self._add_revision(cmd, kwargs, optional=1)
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def propget(self, propname, targets, **kwargs):
+ cmd = ["propget", propname, targets]
+ if kwargs.get("revprop"):
+ cmd.append("--revprop")
+ self._add_revision(cmd, kwargs)
+ status, output = self._execsvn(local=True, *cmd, **kwargs)
+ return output
+
+ def propset(self, propname, value, targets, **kwargs):
+ cmd = ["propset", propname, "'%s'" % value, targets]
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def propedit(self, propname, target, **kwargs):
+ cmd = ["propedit", propname, target]
+ if kwargs.get("rev"):
+ cmd.append("--revprop")
+ self._add_revision(cmd, kwargs)
+ return self._execsvn_success(local=True, show=True, *cmd, **kwargs)
+
+ def revision(self, path, **kwargs):
+ cmd = ["info", path]
+ status, output = self._execsvn(local=True, *cmd, **kwargs)
+ if status == 0:
+ for line in output.splitlines():
+ if line.startswith("Last Changed Rev: "):
+ return int(line.split()[3])
+ return None
+
+ def info(self, path, **kwargs):
+ cmd = ["info", path]
+ status, output = self._execsvn(local=True, noerror=True, *cmd, **kwargs)
+ if "Not a versioned resource" not in output:
+ return output.splitlines()
+ return None
+
+ def info2(self, *args, **kwargs):
+ lines = self.info(*args, **kwargs)
+ if lines is None:
+ return None
+ pairs = [[w.strip() for w in line.split(":", 1)] for line in lines]
+ info = dict(pairs)
+ return info
+
+ def ls(self, path, **kwargs):
+ cmd = ["ls", path]
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return output.split()
+ return None
+
+ def status(self, path, **kwargs):
+ cmd = ["status", path]
+ if kwargs.get("verbose"):
+ cmd.append("-v")
+ if kwargs.get("noignore"):
+ cmd.append("--no-ignore")
+ if kwargs.get("quiet"):
+ cmd.append("--quiet")
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return [x.split() for x in output.splitlines()]
+ return None
+
+ def cleanup(self, path, **kwargs):
+ cmd = ["cleanup", path]
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def revert(self, path, **kwargs):
+ cmd = ["revert", path]
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return [x.split() for x in output.split()]
+ return None
+
+ def switch(self, url, oldurl=None, path=None, relocate=False, **kwargs):
+ cmd = ["switch"]
+ if relocate:
+ if oldurl is None:
+ raise Error, "You must supply the old URL when "\
+ "relocating working copies"
+ cmd.append("--relocate")
+ cmd.append(oldurl)
+ cmd.append(url)
+ if path is not None:
+ cmd.append(path)
+ return self._execsvn_success(*cmd, **kwargs)
+
+ def update(self, path, **kwargs):
+ cmd = ["update", path]
+ self._add_revision(cmd, kwargs, optional=1)
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return [x.split() for x in output.split()]
+ return None
+
+ def merge(self, url1, url2=None, rev1=None, rev2=None, path=None,
+ **kwargs):
+ cmd = ["merge"]
+ if rev1 and rev2 and not url2:
+ cmd.append("-r")
+ cmd.append("%s:%s" % (rev1, rev2))
+ cmd.append(url1)
+ else:
+ if not url2:
+ raise ValueError, \
+ "url2 needed if two revisions are not provided"
+ if rev1:
+ cmd.append("%s@%s" % (url1, rev1))
+ else:
+ cmd.append(url1)
+ if rev2:
+ cmd.append("%s@%s" % (url2, rev2))
+ else:
+ cmd.append(url2)
+ if path:
+ cmd.append(path)
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return [x.split() for x in output.split()]
+ return None
+
+ def diff(self, pathurl1, pathurl2=None, **kwargs):
+ cmd = ["diff", pathurl1]
+ self._add_revision(cmd, kwargs, optional=1)
+ if pathurl2:
+ cmd.append(pathurl2)
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return output
+ return None
+
+ def cat(self, url, **kwargs):
+ cmd = ["cat", url]
+ self._add_revision(cmd, kwargs, optional=1)
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status == 0:
+ return output
+ return None
+
+ def log(self, url, start=None, end=0, limit=None, **kwargs):
+ cmd = ["log", "-v", url]
+ if start is not None or end != 0:
+ if start is not None and type(start) is not type(0):
+ try:
+ start = int(start)
+ except (ValueError, TypeError):
+ raise Error, "invalid log start revision provided"
+ if type(end) is not type(0):
+ try:
+ end = int(end)
+ except (ValueError, TypeError):
+ raise Error, "invalid log end revision provided"
+ start = start or "HEAD"
+ cmd.append("-r %s:%s" % (start, end))
+ if limit is not None:
+ try:
+ limit = int(limit)
+ except (ValueError, TypeError):
+ raise Error, "invalid limit number provided"
+ cmd.append("--limit %d" % limit)
+ status, output = self._execsvn(*cmd, **kwargs)
+ if status != 0:
+ return None
+
+ revheader = re.compile("^r(?P<revision>[0-9]+) \| (?P<author>[^\|]+) \| (?P<date>[^\|]+) \| (?P<lines>[0-9]+) (?:line|lines)$")
+ changedpat = re.compile(r"^\s+(?P<action>[^\s]+) (?P<path>[^\s]+)(?: \([^\s]+ (?P<from_path>[^:]+)(?:\:(?P<from_rev>[0-9]+))?\))?$")
+ logseparator = "-"*72
+ linesleft = 0
+ entry = None
+ log = []
+ appendchanged = 0
+ changedheader = 0
+ for line in output.splitlines():
+ line = line.rstrip()
+ if changedheader:
+ appendchanged = 1
+ changedheader = 0
+ elif appendchanged:
+ if not line:
+ appendchanged = 0
+ continue
+ m = changedpat.match(line)
+ if m:
+ changed = m.groupdict().copy()
+ from_rev = changed.get("from_rev")
+ if from_rev is not None:
+ try:
+ changed["from_rev"] = int(from_rev)
+ except (ValueError, TypeError):
+ raise Error, "invalid revision number in svn log"
+ entry.changed.append(changed)
+ elif linesleft == 0:
+ if line != logseparator:
+ m = revheader.match(line)
+ if m:
+ linesleft = int(m.group("lines"))
+ timestr = " ".join(m.group("date").split()[:2])
+ timetuple = time.strptime(timestr,
+ "%Y-%m-%d %H:%M:%S")
+ entry = SVNLogEntry(int(m.group("revision")),
+ m.group("author"), timetuple)
+ log.append(entry)
+ changedheader = 1
+ else:
+ entry.lines.append(line)
+ linesleft -= 1
+ log.sort()
+ log.reverse()
+ return log
+
+class SVNLook:
+ def __init__(self, repospath, txn=None, rev=None):
+ self.repospath = repospath
+ self.txn = txn
+ self.rev = rev
+
+ def _execsvnlook(self, cmd, *args, **kwargs):
+ execcmd_args = ["svnlook", cmd, self.repospath]
+ self._add_txnrev(execcmd_args, kwargs)
+ execcmd_args += args
+ execcmd_kwargs = {}
+ keywords = ["show", "noerror"]
+ for key in keywords:
+ if kwargs.has_key(key):
+ execcmd_kwargs[key] = kwargs[key]
+ return execcmd(*execcmd_args, **execcmd_kwargs)
+
+ def _add_txnrev(self, cmd_args, received_kwargs):
+ if received_kwargs.has_key("txn"):
+ txn = received_kwargs.get("txn")
+ if txn is not None:
+ cmd_args += ["-t", txn]
+ elif self.txn is not None:
+ cmd_args += ["-t", self.txn]
+ if received_kwargs.has_key("rev"):
+ rev = received_kwargs.get("rev")
+ if rev is not None:
+ cmd_args += ["-r", rev]
+ elif self.rev is not None:
+ cmd_args += ["-r", self.rev]
+
+ def changed(self, **kwargs):
+ status, output = self._execsvnlook("changed", **kwargs)
+ if status != 0:
+ return None
+ changes = []
+ for line in output.splitlines():
+ line = line.rstrip()
+ if not line:
+ continue
+ entry = [None, None, None]
+ changedata, changeprop, path = None, None, None
+ if line[0] != "_":
+ changedata = line[0]
+ if line[1] != " ":
+ changeprop = line[1]
+ path = line[4:]
+ changes.append((changedata, changeprop, path))
+ return changes
+
+ def author(self, **kwargs):
+ status, output = self._execsvnlook("author", **kwargs)
+ if status != 0:
+ return None
+ return output.strip()
+
+# vim:et:ts=4:sw=4
diff --git a/RepSys/util.py b/RepSys/util.py
new file mode 100644
index 0000000..84840b9
--- /dev/null
+++ b/RepSys/util.py
@@ -0,0 +1,141 @@
+#!/usr/bin/python
+
+from RepSys import Error, config
+
+import subprocess
+import getpass
+import sys
+import os
+import re
+import logging
+from cStringIO import StringIO
+#import commands
+
+log = logging.getLogger("repsys")
+
+# Our own version of commands' getstatusoutput(). We have a commands
+# module directory, so we can't import Python's standard module
+def commands_getstatusoutput(cmd):
+ """Return (status, output) of executing cmd in a shell."""
+ import os
+ pipe = os.popen('{ ' + cmd + '; } 2>&1', 'r')
+ text = pipe.read()
+ sts = pipe.close()
+ if sts is None: sts = 0
+ if text[-1:] == '\n': text = text[:-1]
+ return sts, text
+
+def execcmd(*cmd, **kwargs):
+ cmdstr = " ".join(cmd)
+ if kwargs.get("show"):
+ if kwargs.get("geterr"):
+ err = StringIO()
+ pipe = subprocess.Popen(cmdstr, shell=True,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ of = pipe.stdout.fileno()
+ ef = pipe.stderr.fileno()
+ while True:
+ odata = os.read(of, 8192)
+ sys.stdout.write(odata)
+ edata = os.read(ef, 8192)
+ err.write(edata)
+ sys.stderr.write(edata)
+ status = pipe.poll()
+ if status is not None and not (odata and edata):
+ break
+ output = err.getvalue()
+ else:
+ status = os.system(cmdstr)
+ output = ""
+ else:
+ status, output = commands_getstatusoutput(
+ "LANG=C LANGUAGE=C LC_ALL=C "+cmdstr)
+ verbose = config.getbool("global", "verbose", 0)
+ if status != 0 and not kwargs.get("noerror"):
+ if kwargs.get("cleanerr") and not verbose:
+ raise Error, output
+ else:
+ raise Error, "command failed: %s\n%s\n" % (cmdstr, output)
+ if verbose:
+ print cmdstr
+ sys.stdout.write(output)
+ return status, output
+
+def get_auth(username=None, password=None):
+ set_username = 1
+ set_password = 1
+ if not username:
+ username = config.get("auth", "username")
+ if not username:
+ username = raw_input("username: ")
+ else:
+ set_username = 0
+ if not password:
+ password = config.get("auth", "password")
+ if not password:
+ password = getpass.getpass("password: ")
+ else:
+ set_password = 0
+ if set_username:
+ config.set("auth", "username", username)
+ if set_password:
+ config.set("auth", "password", password)
+ return username, password
+
+
+def mapurl(url):
+ """Maps a url following the regexp provided by the option url-map in
+ repsys.conf
+ """
+ urlmap = config.get("global", "url-map")
+ newurl = url
+ if urlmap:
+ try:
+ expr_, replace = urlmap.split()[:2]
+ except ValueError:
+ log.error("invalid url-map: %s", urlmap)
+ else:
+ try:
+ newurl = re.sub(expr_, replace, url)
+ except re.error, errmsg:
+ log.error("error in URL mapping regexp: %s", errmsg)
+ return newurl
+
+
+def get_helper(name):
+ """Tries to find the path of a helper script
+
+ It first looks if the helper has been explicitly defined in
+ configuration, if not, falls back to the default helper path, which can
+ also be defined in configuration file(s).
+ """
+ helperdir = config.get("helper", "prefix", "/usr/share/repsys")
+ hpath = config.get("helper", name, None) or \
+ os.path.join(helperdir, name)
+ if not os.path.isfile(hpath):
+ log.warn("providing unexistent helper: %s", hpath)
+ return hpath
+
+def rellink(src, dst):
+ """Creates relative symlinks
+
+ It will find the common ancestor and append to the src path.
+ """
+ asrc = os.path.abspath(src)
+ adst = os.path.abspath(dst)
+ csrc = asrc.split(os.path.sep)
+ cdst = adst.split(os.path.sep)
+ dstname = cdst.pop()
+ i = 0
+ l = min(len(csrc), len(cdst))
+ while i < l:
+ if csrc[i] != cdst[i]:
+ break
+ i += 1
+ dstextra = len(cdst[i:])
+ steps = [os.path.pardir] * dstextra
+ steps.extend(csrc[i:])
+ return os.path.sep.join(steps)
+
+
+# vim:et:ts=4:sw=4
diff --git a/create-srpm b/create-srpm
new file mode 100755
index 0000000..373b072
--- /dev/null
+++ b/create-srpm
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+
+from RepSys import Error, config, plugins, layout
+from RepSys.mirror import strip_username
+from RepSys.rpmutil import get_srpm
+from RepSys.cgiutil import get_targets
+from RepSys.util import mapurl, execcmd, get_helper
+import sys
+import os
+import pwd
+import optparse
+import subprocess
+import urlparse
+import urllib
+
+class CmdError(Error): pass
+
+class CmdIface:
+ def author_email(self, author):
+ return config.get("users", author)
+
+ def submit_package(self, urls, revision, targetname, dontmapurl_=0,
+ define=[]):
+ pw = pwd.getpwuid(os.getuid())
+ username = pw[0]
+ packager = config.get("users", username) or pw[4]
+ if not packager:
+ raise CmdError, "your email was not found"
+ elif not targetname:
+ raise CmdError, "no target provided"
+ else:
+ targetname = targetname.lower()
+ for target in get_targets():
+ if target.name.lower() == targetname:
+ break
+ else:
+ raise CmdError, "target not found"
+ for url in urls:
+ url = strip_username(url)
+ for allowed in target.allowed:
+ if url.startswith(allowed):
+ break
+ else:
+ raise CmdError, "%s is not allowed for this target" \
+ % url
+ if not dontmapurl_: #FIXME don't use it!
+ urls = [mapurl(url) for url in urls]
+ uploadsrpms = []
+ for url in urls:
+ urlrev = revision or layout.get_url_revision(url)
+ url, _ = layout.split_url_revision(url)
+ targetsrpms = get_srpm(url,
+ revision=urlrev,
+ targetdirs=target.target,
+ packager=packager,
+ svnlog=1,
+ revname=1,
+ scripts=target.scripts,
+ macros=target.macros)
+ uploadsrpms.extend(targetsrpms)
+ uploadcmd = get_helper("upload-srpm")
+ if uploadcmd:
+ upload_command = [uploadcmd]
+ if define:
+ for x in define:
+ upload_command.append("--define")
+ upload_command.append(x)
+ upload_command.append(targetname)
+ upload_command.extend(uploadsrpms)
+ command = subprocess.list2cmdline(upload_command)
+ status, output = execcmd(command, noerror=1)
+ for srpm in uploadsrpms:
+ if os.path.isfile(srpm):
+ os.unlink(srpm)
+ else:
+ sys.stderr.write("warning: temporary file "\
+ "'%s' removed unexpectedly\n" % srpm)
+ if status != 0:
+ raise CmdError, "Failed to upload "\
+ "%s:\n%s" % (" ".join(urls), output)
+ return 1
+
+ def submit_targets(self):
+ return [x.name for x in get_targets()]
+
+
+def parse_options():
+ usage = "create-srpm <packageurl> -t <target>"
+ parser = optparse.OptionParser(usage=usage)
+ parser.add_option("-t", "--target", type="string", dest="target",
+ help="target name")
+ parser.add_option("-M", "--nomapping", action="store_true",
+ dest="urlmap", default=False,
+ help="disable url mapping")
+ parser.add_option("--define", action="append")
+ parser.add_option("--list", dest="list_targets", default=False,
+ action="store_true",
+ help="list submit targets available")
+ parser.add_option("-r", help="revision", dest="revision",
+ type="int", default=None)
+ opts, args = parser.parse_args()
+ if not opts.list_targets and not args:
+ parser.error("you must supply a package url")
+ return opts, args
+
+
+def main():
+ plugins.load()
+ iface = CmdIface()
+ opts, args = parse_options()
+ try:
+ if opts.list_targets:
+ for target in iface.submit_targets():
+ print target
+ else:
+ iface.submit_package(args, opts.revision, opts.target, opts.urlmap,
+ opts.define)
+ except Error, e:
+ sys.stderr.write("error: %s\n" % str(e))
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
+
+# vim:ts=4:sw=4:et
diff --git a/default.chlog b/default.chlog
new file mode 100644
index 0000000..5b9d435
--- /dev/null
+++ b/default.chlog
@@ -0,0 +1,41 @@
+##
+## Default changelog format for Mandriva Linux
+##
+#if not $releases_by_author[-1].visible
+ ## Hide the first release that contains no changes. It must be a
+ ## reimported package and the log gathered from misc/ already should
+ ## contain a correct entry for the version-release:
+ #set $releases_by_author = $releases_by_author[:-1]
+#end if
+#for $rel in $releases_by_author
+* $rel.date $rel.author_name <$rel.author_email> $rel.version-$rel.release
++ Revision: $rel.revision
+## #if not $rel.released
+##+ Status: not released
+## #end if
+ #if not $rel.visible
++ rebuild (emptylog)
+ #end if
+ #for $rev in $rel.release_revisions
+ #for $line in $rev.lines
+$line
+ #end for
+ #end for
+
+ #for $author in $rel.authors
+ #if not $author.visible
+ #continue
+ #end if
+ ##alternatively, one could use:
+ ###if $author.email == "root"
+ ## #continue
+ ###end if
+ + $author.name <$author.email>
+ #for $rev in $author.revisions
+ #for $line in $rev.lines
+ $line
+ #end for
+ #end for
+
+ #end for
+#end for
diff --git a/repsys b/repsys
new file mode 100755
index 0000000..99847bf
--- /dev/null
+++ b/repsys
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+from RepSys import Error, plugins, config
+from RepSys.command import *
+import getopt
+import sys
+
+VERSION="1.9.1-binrepo"
+
+HELP = """\
+Usage: repsys COMMAND [COMMAND ARGUMENTS]
+
+Tool to access and manage a package repository structure.
+
+http://wiki.mandriva.com/en/Development/Packaging/RepositorySystem/Quickstart
+
+Useful commands:
+ co checkout a package
+ ci commit changes
+ sync add-remove all file changes from the .spec
+ submit submit a package for build
+ putsrpm import a source package to the repository
+ getspec prints the spec
+ rpmlog prints the RPM changelog
+ getsrpm creates the source RPM
+ create create the structure of a new package
+ changed shows changes not submitted
+ authoremail prints the e-mail of a given author
+ switch relocate to mirror or upstream repository
+
+Run "repsys COMMAND --help" and "man 8 repsys" for more information.
+
+Written by Gustavo Niemeyer <gustavo@niemeyer.net>
+"""
+
+command_aliases = {"import": "putsrpm"}
+
+def plugin_help(opt, val, parser, mode):
+ if parser is None:
+ prog = sys.argv[0]
+ print "Use %s --help-plugin <plugin name>" % prog
+ print "Available plugins:"
+ print
+ for name in plugins.list():
+ print name
+ else:
+ print plugins.help(parser)
+ raise SystemExit
+
+def parse_options():
+ parser = OptionParser(help=HELP, version="%prog "+VERSION)
+ parser.disable_interspersed_args()
+ parser.add_option("--debug", action="store_true")
+ parser.add_option("--help-plugins", action="callback", callback=plugin_help)
+ parser.add_option("--help-plugin", type="string", dest="__ignore",
+ action="callback", callback=plugin_help)
+ opts, args = parser.parse_args()
+ del opts.__ignore
+ if len(args) < 1:
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+ opts.command = args[0]
+ opts.argv = args
+ return opts
+
+def dispatch_command(command, argv, debug=0):
+ sys.argv = argv
+ try:
+ command = command_aliases[command]
+ except KeyError:
+ pass
+ if debug:
+ config.set("global", "verbose", "yes")
+ try:
+ repsys_module = __import__("RepSys.commands."+command)
+ commands_module = getattr(repsys_module, "commands")
+ command_module = getattr(commands_module, command)
+ except (ImportError, AttributeError):
+ etype, exc, tb = sys.exc_info()
+ if tb.tb_next is None and not debug:
+ raise Error, "invalid command '%s'" % command
+ raise
+ command_module.main()
+
+if __name__ == "__main__":
+ try:
+ plugins.load()
+ except Error, e:
+ sys.stderr.write("plugin initialization error: %s\n" % e)
+ sys.exit(1)
+ config.set("global", "repsys-cmd", sys.argv[0])
+ do_command(parse_options, dispatch_command)
+
+# vim:et:ts=4:sw=4
diff --git a/repsys-example.conf b/repsys-example.conf
new file mode 100644
index 0000000..34e180b
--- /dev/null
+++ b/repsys-example.conf
@@ -0,0 +1,74 @@
+[global]
+verbose = no
+repository = svn+ssh://svn.mandriva.com/svn/packages/
+url-map = svn\+ssh://svn\.mandriva\.com/(.*) file:///\1
+#tempdir = /tmp
+## the command used to download files when using repsys sync -d
+#download-command = wget -c -O '$dest' $url
+svn-env = SVN_SSH=/home/me/my-ssh-wrapper
+ SVN_FOO=bar
+
+[log]
+oldurl = svn+ssh://svn.mandriva.com/svn/packages/misc
+sort = yes
+merge-spec = no
+# controls up to which revision the rpm changelog
+# will be constructed (default zero, i.e., oldest
+# commit)
+revision-offset = 0
+# commit lines containing this string won't be shown in the changelog:
+ignore-string = SILENT
+# and in case of only allowing a few lines to be visible, use this:
+#unignore-string = CLOG
+
+[template]
+# set the cheetah template used to generate the spec changelog from svn
+path = /usr/share/repsys/default.chlog
+
+[helper]
+# create-srpm is called by repsys on the server-side when submitting a
+# package
+create-srpm = /usr/share/repsys/create-srpm
+# upload-srpm is called by create-srpm to copy the generated .src.rpm to
+# the proper build queue
+upload-srpm = /usr/local/bin/youri.devel
+
+# this section maps usernames found in svn commits to the ones that must be
+# shown in the changelog
+# users can be retrieved from LDAP through the use of the repsys-ldap
+# plugin
+[users]
+# jsmith = John Smith <jsmith26@example.com>
+#
+
+[srpm]
+# runs "make -C current/ srpm-prep" on the package before creating the
+# srpm file
+run-prep = yes
+
+[submit]
+host = kenobi.mandriva.com
+default = Cooker
+
+[submit Cooker]
+target = /export/home/repsys
+allowed = svn+ssh://svn.mandriva.com/svn/packages/cooker
+scripts = /usr/share/repsys/rebrand-mdk
+##
+## rpm-macros refers to the sections containing the macros used for this
+## target. The values will be used to build the rpmbuild command line. For
+## example:
+##
+## [macros cooker]
+## a = b
+## c = %a
+##
+## will render in the command line: --define "a b" --define "c %a".
+##
+#rpm-macros = global cooker
+
+#[macros global]
+#distsuffix = mdv
+
+#[macros cooker]
+#mandriva_release = 2007.1
diff --git a/repsys-ssh b/repsys-ssh
new file mode 100755
index 0000000..9532421
--- /dev/null
+++ b/repsys-ssh
@@ -0,0 +1,2 @@
+#!/bin/sh
+ssh -o "BatchMode yes" $*
diff --git a/repsys.8 b/repsys.8
new file mode 100644
index 0000000..a0b240b
--- /dev/null
+++ b/repsys.8
@@ -0,0 +1,218 @@
+.\" repsys - Package repository management tool
+.TH "repsys" "8" "2008 Feb 8" "Mandriva Linux" ""
+.SH "NAME"
+repsys \- Package sources repository management tool
+.SH "SYNOPSIS"
+\fBrepsys\fP command [options] [arguments]
+.SH "DESCRIPTION"
+\fBrepsys\fP is the tool used to manage RPM packages in a subversion repository. It is used to create, tag releases, generate .src.rpm, generate changelog, and request new package releases for build. It mostly acts as a interface to svn(1) commands and small task scripts run on the build system side over ssh(1).
+
+Most of the \fBrepsys\fP commands operate on a given package URL, these URLs can be omitted when the configuration option \fBrepository\fP is set.
+
+Detailed help on commands is available running \fBrepsys <command> \-\-help\fP.
+.SH "BASIC USAGE"
+.SS "Setup"
+\fBrepsys\fP does not handle the authentication interface used by svn. So it is usually required to setup ssh\-agent(1) if the repository access method is over SSH (svn+ssh:// URLs), or performing some simple operation in order to obtain a authentication token.
+
+.nf
+For more information related how to setup ssh-agent, see:
+http://wiki.mandriva.com/en/Development/Docs/Contributor_Tricks#SSH_configuration
+.fi
+
+Users that don't have an ssh account in the default repository URL can set the option \fBmirror\fP pointing to a non-authenticated, read-only repository (such as http://svn.mandriva.com/svn/packages).
+.SS "Examples"
+.PP
+.IP "\fBrepsys co foo\fP"
+Obtains a working copy of the package foo.
+.IP "\fBrepsys co 2009.0/mutt\fP"
+Obtains a working copy of the package mutt of from the 2009.0 branch.
+.IP "\fBrepsys ci\fP"
+Commits pending changes in the working copy.
+.IP "\fBrepsys submit foo \-r 12345\fP"
+Requests the package foo in the revision 12345 to be built and, if successful, to be uploaded to the RPMs repository.
+.IP "\fBrepsys submit foo \-r 12345 -t 2008.0 \-\-define section=main/testing\fP"
+Will submit the package foo and, upon successful build will have its RPMs placed inside the main/testing media of the 2008.0 repository.
+.IP "\fBrepsys submit\fP"
+submit run without parameters will use package name and revision found in the working copy in the current directory.
+.SH "COMMANDS"
+For detailed help on commands run \fBrepsys <command> \-\-help\fP.
+\#TODO complete list of commands, all options, all descriptions
+.PP
+.IP "\fBco\fP"
+checkout a package
+.IP "\fBci\fP"
+commit changes
+.IP "\fBsubmit\fP"
+submit a package in a given revision for build and release
+.IP "\fBsync\fP"
+add-remove all file changes from the .spec
+.IP "\fBgetspec\fP"
+prints the spec
+.IP "\fBrpmlog\fP"
+prints the RPM changelog generated from SVN
+.IP "\fBgetsrpm\fP"
+creates the source RPM
+.IP "\fBcreate\fP"
+create the structure of a new package
+.IP "\fBchanged\fP"
+shows changes not submitted
+.IP "\fBauthoremail\fP"
+prints the e-mail of a given svn author
+.IP "\fBswitch\fP"
+relocate to mirror or upstream repository
+.IP "\fBmarkrelease\fP"
+creates a tag for a given package revision and version
+.SH "REPOSITORY LAYOUT"
+.nf
+A detailed description can be found at:
+http://wiki.mandriva.com/en/Development/Packaging/RepositorySystem
+.fi
+
+The svn repository used by \fBrepsys\fP consists of a set of branches in the top directory, followed by package directories having the internal package structure.
+
+The internal package layout contains a directory \fBcurrent/\fP, which contains the latest version of the package (equivalent to "trunk" in software repositories). The directory \fBreleases/\fP contain copies of older submitted packages that have been already released, it is organized in the \fB<version>/<release>\fP format (equivalent to "tags" directories).
+
+URLs used in \fBrepsys\fP commands refer to the package directory, and never to \fBcurrent\fP. In other words, the http://host/svn/cooker/foo is valid, whereas http://host/svn/cooker/foo/current is not.
+
+One example layout:
+
+\fB
+/packages/cooker
+ |
+ +\- cooker/
+ | ...
+ | +\- rsync/
+ | +\- coreutils/
+ | +\- make/
+ | +\- foo/
+ | +\- current/
+ | | +\- SOURCES/
+ | | +\- SPECS/
+ | +\- releases/
+ | ...
+ | +\- 1.0
+ | +\- 1mdk/
+ | +\- SOURCES/
+ | +\- SPECS/
+ | +\- 2mdk/
+ | ...
+ +\- updates/
+ +\- 2007.0/
+ +\- 2007.1/
+ +\- 2008.0/
+\fP
+.SS "Setting up a repository"
+The minimal setup is accomplished with a Subversion repository having three directories: \fBcooker/\fP, \fBmisc/\fP and \fBupdates/\fP. The name of this directories can be changed using the configuration options \fBtrunk\-dir\fP and \fBbranches-dir\fP. Having this you can start importing packages with \fBrepsys import\fP.
+\#.SH "THE SUBMIT PROCESS"
+\#.SS "Connecting"
+\#.SS "Changelog generation"
+\#.SS "Uploading"
+\#.SH CHANGELOGS
+\#.SH SERVER\-SIDE SETUP
+.SH "CONFIGURATION"
+.SS "Introduction"
+The main configuration file is \fB/etc/repsys.conf\fP, it is in the .ini format. It is basically defined by a set of \fB[name]\fP sections, with a set of variables defined by \fBname = value\fP.
+
+If existing, the file ~/.repsys/config is also loaded.
+.SS "[global] section"
+.PP
+.IP "\fBrepository = URL\fP"
+Contains the base URL used to access packages in the svn repository when only package names are used in repsys commands. For example, if \fBrepsys co trafshow\fP is run and repository is http://host/svn/, the URL http://host/svn/cooker/trafshow will be used ("cooker" is the default branch).
+.IP "\fBdefault_parent = URL\fP"
+Points to the base URL of the development branch of the svn repository. This option is deprecated as it has been replaced by "repository".
+.IP "\fBmirror = URL\fP"
+The URL of an alternative and read\-only repository to be used when checking out packages. \fBrepsys ci\fP will automatically relocate to "repository" when comitting.
+.IP "\fBuse-mirror = yes/no\fP"
+Disable the use of the mirror repository when checking out packages.
+.IP "\fBurl\-map = MATCH\-REGEXP REPLACE\-EXPR\fP"
+This option is used on server-side to remap remote URLs brought by the user when running \fBrepsys submit\fP to local (and probably faster) URLs. \fBMATCH\-REGEXP\fP is a Python regular expression matching the components that must be reused in the local URL. \fbREPLACE\-EXPR\fP is a replace expression that should expand in the final URL. Example: \fBsvn\+ssh://svn\.mandriva\.com/(.*) file:///\1\fP
+.IP "\fBtempdir = PATH\fP"
+The directory to be used as base for temporay directories and files created by repsys.
+.IP "\fBdownload\-command = COMMAND\-FMT\fP"
+Command used to download generic remote URLs, it accepts the variables \fB$url\fP and \fB$dest\fP. It is currently used when running \fBrepsys sync \-d\fP.
+.IP "\fBsvn\-command = COMMAND\fP"
+The base command used to execute svn(1). Runs through system(3).
+.IP "\fBsvn-env = VAR=VALUE ..\fP"
+The environment variables to use when running svn. More entries can be defined by using more lines. The variable defined by default is \fBSVN_SSH\fP, which points to the \fBrepsys-ssh\fP ssh wrapper.
+.IP "\fBverbose = yes/no\fP"
+Increase the verbosity of repsys output, printing commands being run and complete traceback when unhanlded errors happen.
+.IP "\fBtrunk-dir\fP"
+Points to the default branch of the distro used in commands that do not have their branch or URL specified.
+.IP "\fBbranches-dir\fP"
+The directory inside the repository which contains all the branches of the distro. It is used to build the URL of packages referred using the branch notation BRANCH/PACKAGE, as in \fBrepsys co 2009.0/mutt\fP.
+.SS "[submit-groups] section"
+This section contains aliases to groups of packages to be submitted at once. For example, a line with \fBmy-python-packages = bzr bzrtools bzr-gtk\fP would allow the user to simply run \fBrepsys submit my-python-packages\fP.
+
+Also distro branches or revision numbers can be specified for each package group. For example: \fBrepsys submit 2008.1/my-python-packages\fP.
+.SS "[submit] section"
+.IP "\fBhost = HOST\fP"
+Defines the default host in which \fBrepsys submit\fP will run the submit helper.
+.IP "\fBdefault = TARGET\fP"
+The default target to be used in \fBrepsys submit\fP when the option \-t is not used.
+.SS "[submit TARGET] sections (server\-side only)"
+These sections describe each one of the sections available to submit packages, ther configuration options are:
+.IP "\fBtarget = PATH\fP"
+The path where SRPMs generated by \fBcreate\-srpm-\fP will be placed during during the submit process.
+.IP "\fBallowed = URLs\fP"
+A space\-delimited list of package URLs that will be allowed to be used with this target. The comparison is done by checking if the package URL used in submit starts with one of the URLs of this option.
+.IP "\fBscripts = PATHS\fP"
+A space\-delimited list of scripts that will be run receiving the generated SRPM as first argument. These scripts are usually used to perform small changes in the SRPM structure, increasing release number for example.
+.IP "\fBrpm\-macros = NAMES\fP"
+It points to sections in the configuration that will contain the RPM macros used when generating the SRPM of the package being submitted. These section should be named in the \fB[macros NAME]\fP format.
+.SS "[macros NAME] sections (server\-side only)"
+These sections contain variables that will be defined as RPM macros when generating the SRPM of the package being submitted.It is usually used to define the distribution suffix that will be used in package releases, such as "mdv2008.1".
+.SS "[users] section (server\-side only)"
+This section maps the usernames found in svn to their real names and e\-mails. It is used when generating the changelog based on commits in svn and by \fBauthoremail\fP. Example: \fBjoe = Joe User <joeuser@host.com>\fP.
+
+This section can be used on client\-side too, but will have no effect in generated changelogs on the server\-side.
+.SS "[helper] section"
+.IP "\fBcreate\-srpm = PATH\fP"
+The path of the script that will be run through ssh on the submit host when running \fBrepsys submit\fP.
+.IP "\fBupload\-srpm = PATH\fP"
+(server\-side only) Path of the script that will be called after the generated SRPM is copied to its target location (see target sections above) and target scripts are run.
+.IP "\fBrpmbuild = COMMAND\fP"
+The command used to call rpmbuild. Note that build options (such as \-bs) are supplied by repsys.
+.SS "[log] section"
+.IP "\fBoldurl = URL\fP"
+The URL of a directory structure that will contain old changelogs of packages that will be appended to the changelog being generated by \fBrpmlog\fP or \fBgetsrpm \-l\fP.
+.IP "\fBmerge\-spec = yes/no\fP"
+If enabled, changelogs generated by \fBrepsys\fP will have the contents of the %changelog found in the .spec file of the package appended.
+.IP "\fBsort = yes/no\fP"
+If enabled, the changelog will be resorted after its generation. It is useful when changelogs found in \fBoldurl\fP or in the .spec's %changelog section are newer than those generated by SVN.
+.IP "\fBrevision\-offset = REVISION\-NUMBER\fP"
+The base revision used to generated changelogs. As in \fBsvn log -r REVISION\-OFFSET:HEAD URL\fP.
+.IP "\fBignore\-string = STRING\fP"
+Mark used to hide log messages. When it appears at the beginning of the log message, the whole changeset log is hidden. When it is found in the middle of a string, only the line will not be shown.
+.IP "\fBunignore\-string = STRING\fP"
+The complement of the previous option. When this token is found, only those lines containg this mark will be shown. It is intended to be used in very long log messages.
+.SS "[template] section"
+.IP "\fBpath = PATH\fP"
+The path of the template used to generate the changelog from svn commits.
+.SS "[srpm] section"
+.IP "\fBrun-prep = yes/no\fP"
+Repsys can check for the presence of a file named \fBMakefile\fP in the top directory of the package and run \fBmake prep-srpm\fP so that it can generate the actual files that must be distributed in in the srpm. This option enables this feature. (Note: the command is expected to run in an restricted environment, the Makefile must use only minimal funcionalities.)
+.SH "ENVIRONMENT VARIABLES"
+.PP
+.IP "\fBREPSYS_CONF\fP"
+Sets the configuration file to be read by \fBrepsys\fP
+.SH "FILES"
+.nf
+~/.repsys/config
+/etc/repsys.conf
+/usr/share/repsys/
+/usr/share/doc/repsys/
+.fi
+.SH "BUGS"
+See the list of bugs at http://qa.mandriva.com/buglist.cgi?quicksearch=repsys
+.SH "SEE ALSO"
+mdvsys(1), svn(1), ssh\-agent(1)
+
+.nf
+http://wiki.mandriva.com/en/Development/Packaging/RepositorySystem/Quickstart
+.fi
+.SH "AUTHOR"
+.nf
+repsys was originally written by Gustavo Niemeyer <gustavo@niemeyer.net>
+for the Conectiva Linux distribution. Currently it is being mantained by
+Mandriva contributors and employees.
+.fi
diff --git a/repsys.conf b/repsys.conf
new file mode 100644
index 0000000..45cda90
--- /dev/null
+++ b/repsys.conf
@@ -0,0 +1,18 @@
+# see man 8 repsys for a description on configuration options
+[global]
+repository = svn+ssh://svn.mandriva.com/svn/packages/
+## uncomment it in case you don't have a account in the Mandriva cluster:
+#mirror = http://svn.mandriva.com/svn/packages/cooker/
+#use-binaries-repository = yes
+#binaries-repository = svn.mandriva.com:/tarballs/
+
+[log]
+oldurl = svn+ssh://svn.mandriva.com/svn/packages/misc
+
+[helper]
+create-srpm = /usr/share/repsys/create-srpm
+upload-srpm = /usr/local/bin/youri.devel
+
+[submit]
+host = kenobi.mandriva.com
+default = Cooker
diff --git a/revno.chlog b/revno.chlog
new file mode 100644
index 0000000..0f78dab
--- /dev/null
+++ b/revno.chlog
@@ -0,0 +1,41 @@
+## Sample Changelog template
+## lcapitulinos' Changelog3
+##
+#for $rel in $releases_by_author
+* $rel.date $rel.author_name <$rel.author_email> $rel.version-$rel.release
+ ##
+ #if not $rel.released
+ (not released yet)
+ #end if
+
+ #for $rev in $rel.release_revisions
+ #set $first = 1
+ #set $spaces = " " * len(str(rev.revision))
+ #for $line in $rev.lines
+ #if $first == 1
+ [$rev.revision] $line
+ #else
+ $spaces $line
+ #end if
+ #set first=0
+ #end for
+ #end for
+
+ #for $author in $rel.authors
+ + $author.name <$author.email>
+ #for $rev in $author.revisions
+ #set first=1
+ #set $spaces = " " * len(str(rev.revision))
+ #for $line in $rev.lines
+ #if $first==1
+ [$rev.revision] $line
+ #else
+ $spaces $line
+ #end if
+ #set first=0
+ #end for
+ #end for
+
+ #end for
+#end for
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..7395d36
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[bdist_rpm]
+doc_files = repsys.conf README README.LDAP CHANGES
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..e11789d
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+from distutils.core import setup
+import sys
+import re
+
+verpat = re.compile("VERSION *= *\"(.*)\"")
+data = open("repsys").read()
+m = verpat.search(data)
+if not m:
+ sys.exit("error: can't find VERSION")
+VERSION = m.group(1)
+
+setup(name="repsys",
+ version = VERSION,
+ description = "Tools for Mandriva Linux repository access and management",
+ author = "Gustavo Niemeyer",
+ author_email = "gustavo@niemeyer.net",
+ url = "http://qa.mandriva.com/twiki/bin/view/Main/RepositorySystem",
+ license = "GPL",
+ long_description = """Tools for Mandriva Linux repository access and management.""",
+ packages = ["RepSys", "RepSys.cgi", "RepSys.commands",
+ "RepSys.plugins"],
+ scripts = ["repsys", "repsys-ssh"],
+ data_files = [
+ ("/usr/share/repsys/",
+ ["default.chlog",
+ "revno.chlog",
+ "create-srpm"]),
+ ("/etc/", ["repsys.conf"]),
+ ("share/man/man8/", ["repsys.8"])]
+ )
+
+# vim:ts=4:sw=4:et