summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichal ÄŒihaÅ™ <michal@cihar.com>2016-07-27 12:52:13 +0200
committerMichal ÄŒihaÅ™ <michal@cihar.com>2016-07-27 12:52:13 +0200
commit947d5bf39c69df920e52daf2dc041448058db249 (patch)
tree01f45984facfa1fa63884fee546a1d1a887f2d1d
parent8eff0f2aa237e66b54f289e474921333e4b7abd6 (diff)
Imported Upstream version 0.154.0
-rw-r--r--.travis.yml12
-rw-r--r--AUTHORS23
-rw-r--r--COPYING340
-rw-r--r--NEWS1093
-rw-r--r--PROJ_PACK.txt92
-rw-r--r--README271
-rw-r--r--TODO87
-rw-r--r--dist/complete.csh14
-rw-r--r--dist/complete.sh6
-rwxr-xr-xdist/osc.complete1826
-rw-r--r--docs/_static/.keepme0
-rw-r--r--docs/api/modules.rst10
-rw-r--r--docs/api/osc.core.rst20
-rw-r--r--docs/api/osc.util.rst78
-rw-r--r--docs/api/tutorial.rst96
-rw-r--r--docs/conf.py298
-rw-r--r--docs/index.rst33
-rwxr-xr-xfuse/fuseosc234
-rwxr-xr-xfuse/start3
-rwxr-xr-xfuse/stop2
-rwxr-xr-xosc-wrapper.py42
-rw-r--r--osc.fish116
-rw-r--r--osc.icobin0 -> 1150 bytes
-rw-r--r--osc.pngbin0 -> 281 bytes
-rw-r--r--osc/OscConfigParser.py360
-rw-r--r--osc/__init__.py3
-rw-r--r--osc/babysitter.py212
-rw-r--r--osc/build.py1110
-rw-r--r--osc/checker.py122
-rw-r--r--osc/cmdln.py1600
-rw-r--r--osc/commandline.py8496
-rw-r--r--osc/conf.py1031
-rw-r--r--osc/core.py7281
-rw-r--r--osc/fetch.py435
-rw-r--r--osc/meter.py103
-rw-r--r--osc/oscerr.py156
-rw-r--r--osc/oscssl.py372
-rw-r--r--osc/oscsslexcp.py8
-rw-r--r--osc/util/__init__.py1
-rw-r--r--osc/util/ar.py214
-rw-r--r--osc/util/archquery.py187
-rw-r--r--osc/util/cpio.py256
-rw-r--r--osc/util/debquery.py191
-rw-r--r--osc/util/packagequery.py164
-rw-r--r--osc/util/repodata.py176
-rw-r--r--osc/util/rpmquery.py366
-rw-r--r--osc/util/safewriter.py29
-rwxr-xr-xosc_expand_link.pl491
-rwxr-xr-xosc_hotshot.py32
-rwxr-xr-xrun_bandit.sh18
-rwxr-xr-xsetup.py98
-rw-r--r--tests/addfile_fixtures/oscrc103
-rw-r--r--tests/addfile_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/addfile_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/addfile_fixtures/osctest/.osc/_project1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_files5
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/_to_be_deleted1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/addfile_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/addfile_fixtures/osctest/simple/merge4
-rw-r--r--tests/addfile_fixtures/osctest/simple/nochange2
-rw-r--r--tests/addfile_fixtures/osctest/simple/toadd11
-rw-r--r--tests/addfile_fixtures/osctest/simple/toadd21
-rw-r--r--tests/commit_fixtures/oscrc103
-rw-r--r--tests/commit_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/commit_fixtures/osctest/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_files5
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/_to_be_added1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/add/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/add/add1
-rw-r--r--tests/commit_fixtures/osctest/add/exists0
-rw-r--r--tests/commit_fixtures/osctest/add/foo1
-rw-r--r--tests/commit_fixtures/osctest/add/merge4
-rw-r--r--tests/commit_fixtures/osctest/add/nochange1
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_files3
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/added_missing/.osc/_to_be_added2
-rw-r--r--tests/commit_fixtures/osctest/added_missing/bar1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_files8
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_to_be_added2
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/_to_be_deleted1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/missing1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/allstates/.osc/test1
-rw-r--r--tests/commit_fixtures/osctest/allstates/add1
-rw-r--r--tests/commit_fixtures/osctest/allstates/exists0
-rw-r--r--tests/commit_fixtures/osctest/allstates/missing1
-rw-r--r--tests/commit_fixtures/osctest/allstates/nochange1
-rw-r--r--tests/commit_fixtures/osctest/allstates/test1
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_files4
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_meta5
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/branch/.osc/simple1
-rw-r--r--tests/commit_fixtures/osctest/branch/cfilesremote5
-rw-r--r--tests/commit_fixtures/osctest/branch/files4
-rw-r--r--tests/commit_fixtures/osctest/branch/filesremote5
-rw-r--r--tests/commit_fixtures/osctest/branch/simple1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_files5
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_in_conflict1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/conflict/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/conflict/foo1
-rw-r--r--tests/commit_fixtures/osctest/conflict/merge4
-rw-r--r--tests/commit_fixtures/osctest/conflict/nochange1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_files5
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/_to_be_deleted1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/delete/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/delete/exists0
-rw-r--r--tests/commit_fixtures/osctest/delete/foo1
-rw-r--r--tests/commit_fixtures/osctest/delete/merge4
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_files6
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_to_be_added2
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/_to_be_deleted2
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/multiple/.osc/test1
-rw-r--r--tests/commit_fixtures/osctest/multiple/add1
-rw-r--r--tests/commit_fixtures/osctest/multiple/add21
-rw-r--r--tests/commit_fixtures/osctest/multiple/exists0
-rw-r--r--tests/commit_fixtures/osctest/multiple/nochange1
-rw-r--r--tests/commit_fixtures/osctest/multiple/test1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_files5
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/nochanges/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/nochanges/exists0
-rw-r--r--tests/commit_fixtures/osctest/nochanges/nochange1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_files5
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_meta4
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/commit_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/commit_fixtures/osctest/simple/exists0
-rw-r--r--tests/commit_fixtures/osctest/simple/foo1
-rw-r--r--tests/commit_fixtures/osctest/simple/merge4
-rw-r--r--tests/commit_fixtures/osctest/simple/nochange2
-rw-r--r--tests/commit_fixtures/testAddedMissing_cfilesremote4
-rw-r--r--tests/commit_fixtures/testAddedMissing_filesremote3
-rw-r--r--tests/commit_fixtures/testAddedMissing_lfilelist1
-rw-r--r--tests/commit_fixtures/testAddedMissing_missingfilelist3
-rw-r--r--tests/commit_fixtures/testAddfile_cfilesremote6
-rw-r--r--tests/commit_fixtures/testAddfile_filesremote5
-rw-r--r--tests/commit_fixtures/testAddfile_lfilelist1
-rw-r--r--tests/commit_fixtures/testAddfile_missingfilelist3
-rw-r--r--tests/commit_fixtures/testAllStates_cfilesremote8
-rw-r--r--tests/commit_fixtures/testAllStates_expfiles8
-rw-r--r--tests/commit_fixtures/testAllStates_filesremote8
-rw-r--r--tests/commit_fixtures/testAllStates_lfilelist1
-rw-r--r--tests/commit_fixtures/testAllStates_missingfilelist5
-rw-r--r--tests/commit_fixtures/testConflictfile_filesremote5
-rw-r--r--tests/commit_fixtures/testDeletefile_cfilesremote4
-rw-r--r--tests/commit_fixtures/testDeletefile_filesremote5
-rw-r--r--tests/commit_fixtures/testDeletefile_lfilelist1
-rw-r--r--tests/commit_fixtures/testExpand_cfilesremote5
-rw-r--r--tests/commit_fixtures/testExpand_expandedfilesremote4
-rw-r--r--tests/commit_fixtures/testExpand_filesremote5
-rw-r--r--tests/commit_fixtures/testExpand_lfilelist1
-rw-r--r--tests/commit_fixtures/testExpand_missingfilelist3
-rw-r--r--tests/commit_fixtures/testInterrupted_lfilelist1
-rw-r--r--tests/commit_fixtures/testMultiple_cfilesremote6
-rw-r--r--tests/commit_fixtures/testMultiple_filesremote6
-rw-r--r--tests/commit_fixtures/testMultiple_lfilelist1
-rw-r--r--tests/commit_fixtures/testMultiple_missingfilelist5
-rw-r--r--tests/commit_fixtures/testNoChanges_filesremote5
-rw-r--r--tests/commit_fixtures/testPartial_cfilesremote6
-rw-r--r--tests/commit_fixtures/testPartial_filesremote6
-rw-r--r--tests/commit_fixtures/testPartial_lfilelist1
-rw-r--r--tests/commit_fixtures/testPartial_missingfilelist4
-rw-r--r--tests/commit_fixtures/testSimple_cfilesremote5
-rw-r--r--tests/commit_fixtures/testSimple_filesremote5
-rw-r--r--tests/commit_fixtures/testSimple_lfilelist1
-rw-r--r--tests/commit_fixtures/testSimple_missingfilelist3
-rw-r--r--tests/common.py207
-rw-r--r--tests/conf_fixtures/oscrc103
-rw-r--r--tests/deletefile_fixtures/oscrc103
-rw-r--r--tests/deletefile_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/deletefile_fixtures/osctest/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_files5
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_osclib_version1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_package1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_added1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_deleted1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/.osc/nochange1
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/nochange2
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/toadd11
-rw-r--r--tests/deletefile_fixtures/osctest/already_deleted/toadd21
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_files5
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_in_conflict1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_osclib_version1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_package1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/_to_be_added1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/.osc/nochange1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/foo5
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/foo.mine1
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/foo.r21
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/nochange2
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/toadd11
-rw-r--r--tests/deletefile_fixtures/osctest/conflict/toadd21
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_files5
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_osclib_version1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_package1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_to_be_added1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/_to_be_deleted1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/delete/.osc/nochange1
-rw-r--r--tests/deletefile_fixtures/osctest/delete/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/delete/nochange2
-rw-r--r--tests/deletefile_fixtures/osctest/delete/toadd21
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_files5
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_osclib_version1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_package1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/_to_be_added2
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/replace/.osc/nochange1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/merge1
-rw-r--r--tests/deletefile_fixtures/osctest/replace/nochange2
-rw-r--r--tests/deletefile_fixtures/osctest/replace/toadd11
-rw-r--r--tests/deletefile_fixtures/osctest/replace/toadd21
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_files7
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/_to_be_added1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/foo1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/merge4
-rw-r--r--tests/deletefile_fixtures/osctest/simple/nochange2
-rw-r--r--tests/deletefile_fixtures/osctest/simple/skipped_exists1
-rw-r--r--tests/deletefile_fixtures/osctest/simple/toadd11
-rw-r--r--tests/deletefile_fixtures/osctest/simple/toadd21
-rw-r--r--tests/difffile_fixtures/oscrc103
-rw-r--r--tests/difffile_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/difffile_fixtures/osctest/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_files4
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_to_be_added1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/_to_be_deleted1
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/binarybin0 -> 18 bytes
-rw-r--r--tests/difffile_fixtures/osctest/binary/.osc/binary_deletedbin0 -> 26 bytes
-rw-r--r--tests/difffile_fixtures/osctest/binary/binarybin0 -> 27 bytes
-rw-r--r--tests/difffile_fixtures/osctest/binary/binary_addedbin0 -> 7 bytes
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_files5
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/_to_be_deleted1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/.osc/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localdelete/toadd21
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/_files6
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/binarybin0 -> 27 bytes
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/.osc/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/binarybin0 -> 31 bytes
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/nochange2
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/toadd11
-rw-r--r--tests/difffile_fixtures/osctest/remote_localmodified/toadd21
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_files5
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/_to_be_added1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/.osc/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/binarybin0 -> 27 bytes
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/toadd11
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple/toadd21
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_files5
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/foo1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/merge4
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/remote_simple_noadd/toadd21
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_files3
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/_to_be_added1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/.osc/replaced1
-rw-r--r--tests/difffile_fixtures/osctest/replaced/replaced1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_files9
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_in_conflict1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_to_be_added3
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/_to_be_deleted1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/missing1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/replaced1
-rw-r--r--tests/difffile_fixtures/osctest/simple/.osc/somefile1
-rw-r--r--tests/difffile_fixtures/osctest/simple/foo5
-rw-r--r--tests/difffile_fixtures/osctest/simple/merge4
-rw-r--r--tests/difffile_fixtures/osctest/simple/nochange2
-rw-r--r--tests/difffile_fixtures/osctest/simple/replaced1
-rw-r--r--tests/difffile_fixtures/osctest/simple/toadd11
-rw-r--r--tests/difffile_fixtures/osctest/simple/toadd21
-rw-r--r--tests/difffile_fixtures/testDiffRemoteDeletedLocalAdded_files5
-rw-r--r--tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_binarybin0 -> 7 bytes
-rw-r--r--tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_files7
-rw-r--r--tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_foobar2
-rw-r--r--tests/difffile_fixtures/testDiffRemoteMissingLocalDeleted_files4
-rw-r--r--tests/difffile_fixtures/testDiffRemoteMissingLocalExisting_files4
-rw-r--r--tests/difffile_fixtures/testDiffRemoteModified_files5
-rw-r--r--tests/difffile_fixtures/testDiffRemoteModified_merge3
-rw-r--r--tests/difffile_fixtures/testDiffRemoteNoChange_files5
-rw-r--r--tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_binarybin0 -> 27 bytes
-rw-r--r--tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_files6
-rw-r--r--tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_nochange1
-rw-r--r--tests/init_package_fixtures/oscrc103
-rw-r--r--tests/init_project_fixtures/oscrc103
l---------tests/osc1
-rw-r--r--tests/prdiff_fixtures/common-two-diff10
-rw-r--r--tests/prdiff_fixtures/home:user:branches:some:project/.osc/_apiurl1
-rw-r--r--tests/prdiff_fixtures/home:user:branches:some:project/.osc/_packages4
-rw-r--r--tests/prdiff_fixtures/home:user:branches:some:project/.osc/_project1
-rw-r--r--tests/prdiff_fixtures/home:user:branches:some:project/common-two5
-rw-r--r--tests/prdiff_fixtures/home:user:branches:some:project/directory6
-rw-r--r--tests/prdiff_fixtures/new:prj/common-two5
-rw-r--r--tests/prdiff_fixtures/new:prj/directory6
-rw-r--r--tests/prdiff_fixtures/no-requests2
-rw-r--r--tests/prdiff_fixtures/old:prj/common-two4
-rw-r--r--tests/prdiff_fixtures/old:prj/directory6
-rw-r--r--tests/prdiff_fixtures/oscrc103
-rw-r--r--tests/prdiff_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/prdiff_fixtures/osctest/.osc/_packages4
-rw-r--r--tests/prdiff_fixtures/osctest/.osc/_project1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_apiurl1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_files4
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_meta10
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_osclib_version1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_package1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/_project1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/.osc/common-one.spec1
-rw-r--r--tests/prdiff_fixtures/osctest/common-one/common-one.spec1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_apiurl1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_files4
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_meta10
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_osclib_version1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_package1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/_project1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/.osc/common-two.spec1
-rw-r--r--tests/prdiff_fixtures/osctest/common-two/common-two.spec1
-rw-r--r--tests/prdiff_fixtures/request16
-rw-r--r--tests/prdiff_fixtures/some:project/.osc/_apiurl1
-rw-r--r--tests/prdiff_fixtures/some:project/.osc/_packages4
-rw-r--r--tests/prdiff_fixtures/some:project/.osc/_project1
-rw-r--r--tests/prdiff_fixtures/some:project/common-two5
-rw-r--r--tests/prdiff_fixtures/some:project/directory6
-rw-r--r--tests/project_package_status_fixtures/oscrc103
-rw-r--r--tests/project_package_status_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/.osc/_packages9
-rw-r--r--tests/project_package_status_fixtures/osctest/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_files1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_osclib_version1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_package1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/.osc/_to_be_added1
-rw-r--r--tests/project_package_status_fixtures/osctest/added/exists0
-rw-r--r--tests/project_package_status_fixtures/osctest/added/new1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_files4
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_in_conflict1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_osclib_version1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_package1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/conflict1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/.osc/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/conflict1
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/exists0
-rw-r--r--tests/project_package_status_fixtures/osctest/conflict/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_files4
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_osclib_version1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_package1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/_to_be_deleted2
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/modified1
-rw-r--r--tests/project_package_status_fixtures/osctest/deleted/.osc/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/_files4
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/_osclib_version1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/_package1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/modified1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/.osc/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/_linkerror0
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/dir/file1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/exists0
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/foo.orig0
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/modified1
-rw-r--r--tests/project_package_status_fixtures/osctest/excluded/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_files8
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_added3
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_deleted1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/missing1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/.osc/test1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/add1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/exists0
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/missing1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/nochange1
-rw-r--r--tests/project_package_status_fixtures/osctest/simple/test1
-rw-r--r--tests/repairwc_fixtures/oscrc103
-rw-r--r--tests/repairwc_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/repairwc_fixtures/osctest/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/_packages1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildconfig_prj_arch0
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildinfo_prj_arch.xml0
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/foobar0
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/buildfiles/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_files1
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_meta11
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_deleted2
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/.osc/unknown_file0
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/foobar0
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/multiple/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/foobar0
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/noapiurl/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple/toadd21
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple1/toadd21
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/.osc/somefile1
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple2/toadd21
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/.osc/toadd10
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple3/toadd21
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/_to_be_deleted2
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple4/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple5/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple6/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_files6
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/foobar0
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple7/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_files6
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/.osc/skipped0
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/simple8/toadd11
-rw-r--r--tests/repairwc_fixtures/osctest/working_empty/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/working_empty/.osc/_files1
-rw-r--r--tests/repairwc_fixtures/osctest/working_empty/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/working_empty/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/working_empty/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_files5
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_in_conflict1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_osclib_version1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_package1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_added1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_deleted1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/foo1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/.osc/nochange1
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/foobar0
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/merge4
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/nochange2
-rw-r--r--tests/repairwc_fixtures/osctest/working_nonempty/toadd11
-rw-r--r--tests/repairwc_fixtures/prj_invalidapiurl/.osc/_apiurl1
-rw-r--r--tests/repairwc_fixtures/prj_invalidapiurl/.osc/_packages1
-rw-r--r--tests/repairwc_fixtures/prj_invalidapiurl/.osc/_project1
-rw-r--r--tests/repairwc_fixtures/prj_noapiurl/.osc/_packages1
-rw-r--r--tests/repairwc_fixtures/prj_noapiurl/.osc/_project1
-rw-r--r--tests/request_fixtures/oscrc103
-rw-r--r--tests/request_fixtures/test_read_request1.xml18
-rw-r--r--tests/request_fixtures/test_read_request2.xml21
-rw-r--r--tests/request_fixtures/test_request_list_view1.xml36
-rw-r--r--tests/request_fixtures/test_request_list_view2.xml18
-rw-r--r--tests/request_fixtures/test_request_str1.xml30
-rw-r--r--tests/revertfile_fixtures/oscrc103
-rw-r--r--tests/revertfile_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/revertfile_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/revertfile_fixtures/osctest/.osc/_project1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_files10
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_in_conflict1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_to_be_added3
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/_to_be_deleted2
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/deleted0
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/missing1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/replaced1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/.osc/somefile1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/foo5
-rw-r--r--tests/revertfile_fixtures/osctest/simple/merge4
-rw-r--r--tests/revertfile_fixtures/osctest/simple/nochange2
-rw-r--r--tests/revertfile_fixtures/osctest/simple/replaced1
-rw-r--r--tests/revertfile_fixtures/osctest/simple/toadd11
-rw-r--r--tests/revertfile_fixtures/osctest/simple/toadd21
-rw-r--r--tests/setlinkrev_fixtures/expandedsrc_filesremote5
-rw-r--r--tests/setlinkrev_fixtures/link_with_rev1
-rw-r--r--tests/setlinkrev_fixtures/md5_rev_link1
-rw-r--r--tests/setlinkrev_fixtures/noproject_link1
-rw-r--r--tests/setlinkrev_fixtures/oscrc103
-rw-r--r--tests/setlinkrev_fixtures/rev_link1
-rw-r--r--tests/setlinkrev_fixtures/simple_filesremote4
-rw-r--r--tests/setlinkrev_fixtures/simple_link1
-rw-r--r--tests/suite.py48
-rw-r--r--tests/test_addfiles.py85
-rw-r--r--tests/test_commit.py317
-rw-r--r--tests/test_conf.py32
-rw-r--r--tests/test_deletefiles.py207
-rw-r--r--tests/test_difffiles.py336
-rw-r--r--tests/test_init_package.py88
-rw-r--r--tests/test_init_project.py71
-rw-r--r--tests/test_package_status.py86
-rw-r--r--tests/test_prdiff.py271
-rw-r--r--tests/test_project_status.py161
-rw-r--r--tests/test_repairwc.py265
-rw-r--r--tests/test_request.py577
-rw-r--r--tests/test_revertfiles.py97
-rw-r--r--tests/test_setlinkrev.py93
-rw-r--r--tests/test_update.py288
-rw-r--r--tests/update_fixtures/meta.xml8
-rw-r--r--tests/update_fixtures/oscrc103
-rw-r--r--tests/update_fixtures/osctest/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/.osc/_packages1
-rw-r--r--tests/update_fixtures/osctest/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_in_conflict1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_meta8
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/foo1
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/merge2
-rw-r--r--tests/update_fixtures/osctest/already_in_conflict/nochange1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/conflict/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/conflict/foo1
-rw-r--r--tests/update_fixtures/osctest/conflict/merge4
-rw-r--r--tests/update_fixtures/osctest/conflict/nochange1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/_to_be_deleted2
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/deleted/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/deleted/merge3
-rw-r--r--tests/update_fixtures/osctest/deleted/nochange1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/limitsize/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/limitsize/foo1
-rw-r--r--tests/update_fixtures/osctest/limitsize/merge4
-rw-r--r--tests/update_fixtures/osctest/limitsize/nochange2
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/_size_limit1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/foo1
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/merge4
-rw-r--r--tests/update_fixtures/osctest/limitsize_local/nochange2
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_meta_mode0
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/metamode/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/metamode/foo1
-rw-r--r--tests/update_fixtures/osctest/metamode/merge4
-rw-r--r--tests/update_fixtures/osctest/metamode/nochange1
-rw-r--r--tests/update_fixtures/osctest/new/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/new/.osc/_files1
-rw-r--r--tests/update_fixtures/osctest/new/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/new/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/new/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/restore/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/restore/exists0
-rw-r--r--tests/update_fixtures/osctest/restore/merge4
-rw-r--r--tests/update_fixtures/osctest/restore/nochange1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_files6
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_in_update/_files6
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_in_update/foo1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_meta8
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/added1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/resume/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/resume/added1
-rw-r--r--tests/update_fixtures/osctest/resume/exists0
-rw-r--r--tests/update_fixtures/osctest/resume/foo1
-rw-r--r--tests/update_fixtures/osctest/resume/merge4
-rw-r--r--tests/update_fixtures/osctest/resume/nochange1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_files6
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/_files5
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/foo1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_meta8
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/added0
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/merge5
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/added0
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/exists0
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/f1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/foo1
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/merge5
-rw-r--r--tests/update_fixtures/osctest/resume_deleted/nochange1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/services/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/services/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/services/_service:exists2
-rw-r--r--tests/update_fixtures/osctest/services/foo1
-rw-r--r--tests/update_fixtures/osctest/services/merge4
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/_apiurl1
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/_files5
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/_osclib_version1
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/_package1
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/_project1
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/foo1
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/merge4
-rw-r--r--tests/update_fixtures/osctest/simple/.osc/nochange1
-rw-r--r--tests/update_fixtures/osctest/simple/exists0
-rw-r--r--tests/update_fixtures/osctest/simple/foo1
-rw-r--r--tests/update_fixtures/osctest/simple/merge4
-rw-r--r--tests/update_fixtures/osctest/simple/nochange2
-rw-r--r--tests/update_fixtures/testUpdateAlreadyInConflict_files5
-rw-r--r--tests/update_fixtures/testUpdateAlreadyInConflict_merge1
-rw-r--r--tests/update_fixtures/testUpdateConflict_files5
-rw-r--r--tests/update_fixtures/testUpdateConflict_merge4
-rw-r--r--tests/update_fixtures/testUpdateDeletedFile_files4
-rw-r--r--tests/update_fixtures/testUpdateLimitSizeAddDelete_exists1
-rw-r--r--tests/update_fixtures/testUpdateLimitSizeAddDelete_files6
-rw-r--r--tests/update_fixtures/testUpdateLimitSizeAddDelete_filesremote6
-rw-r--r--tests/update_fixtures/testUpdateLimitSizeNoChange_files6
-rw-r--r--tests/update_fixtures/testUpdateLimitSizeNoChange_filesremote6
-rw-r--r--tests/update_fixtures/testUpdateLocalDeletions_files5
-rw-r--r--tests/update_fixtures/testUpdateLocalDeletions_foo2
-rw-r--r--tests/update_fixtures/testUpdateLocalDeletions_merge4
-rw-r--r--tests/update_fixtures/testUpdateLocalLimitSizeNoChange_files6
-rw-r--r--tests/update_fixtures/testUpdateLocalLimitSizeNoChange_filesremote6
-rw-r--r--tests/update_fixtures/testUpdateMetaMode__meta4
-rw-r--r--tests/update_fixtures/testUpdateMetaMode_filesremote3
-rw-r--r--tests/update_fixtures/testUpdateNewFileLocalExists_exists1
-rw-r--r--tests/update_fixtures/testUpdateNewFileLocalExists_files6
-rw-r--r--tests/update_fixtures/testUpdateNewFile_files6
-rw-r--r--tests/update_fixtures/testUpdateNewFile_upstream_added1
-rw-r--r--tests/update_fixtures/testUpdateNew_filesremote2
-rw-r--r--tests/update_fixtures/testUpdateNoChanges_files5
-rw-r--r--tests/update_fixtures/testUpdateRestore_files5
-rw-r--r--tests/update_fixtures/testUpdateRestore_foo1
-rw-r--r--tests/update_fixtures/testUpdateResumeDeletedFile_files5
-rw-r--r--tests/update_fixtures/testUpdateResumeDeletedFile_foo1
-rw-r--r--tests/update_fixtures/testUpdateResumeDeletedFile_merge4
-rw-r--r--tests/update_fixtures/testUpdateResume_files6
-rw-r--r--tests/update_fixtures/testUpdateResume_foo1
-rw-r--r--tests/update_fixtures/testUpdateResume_merge5
-rw-r--r--tests/update_fixtures/testUpdateServiceFilesAddDelete__service:bar1
-rw-r--r--tests/update_fixtures/testUpdateServiceFilesAddDelete__service:foo1
-rw-r--r--tests/update_fixtures/testUpdateServiceFilesAddDelete_bigfile5
-rw-r--r--tests/update_fixtures/testUpdateServiceFilesAddDelete_files7
-rw-r--r--tests/update_fixtures/testUpdateServiceFilesAddDelete_filesremote7
-rw-r--r--tests/update_fixtures/testUpdateUpstreamModifiedFile_files5
-rw-r--r--tests/update_fixtures/testUpdateUpstreamModifiedFile_foo3
957 files changed, 34796 insertions, 0 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bd9914c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: python
+python:
+ - "2.7"
+ - "3.3"
+ - "3.4"
+ - "3.5-dev"
+addons:
+ apt:
+ packages:
+ - diffstat
+sudo: false
+script: cd tests; python suite.py
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..01b5fee
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,23 @@
+Adrian Schroeter
+Andreas Bauer
+Christoph Thiel
+David Mayr
+Dirk Mueller
+Juergen Weigert
+Lars Rupp
+Lenz Grimmer
+Ludwig Nussel
+Marcus Huewe
+Marcus Rueckert
+Martin Mohring
+Michael Schroeder
+Michael Wolf
+Michal Marek
+Pavol Rusnak
+Peter Poeml
+Sascha Peilicke
+Susanne Oberhauser
+Tom Patzig
+Werner Fink
+Will Stephenson
+Jan-Simon Möller
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..c6b1bb9
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..6657f93
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,1093 @@
+0.154
+ - switch to new obs_scm service when adding git URL's
+ - set OSC_VERSION environment for source services
+ (allows to work in local git checkouts when using obs_scm)
+
+0.153
+ - "my sr" is using the server side request collection to get right results
+ - maintenance request offers to supersede old, but still open requests
+ - add build --vm-telnet option for getting debug shell in KVM builds
+ - add buildhistory --limit option
+ OBS 2.7 only:
+ - add "addchannels" and "enablechannel" commands
+ - support new package instances on branching when using -N parameter
+ - add --linkrev option to branch command
+ - add --add-repository-block option to branch command
+ - add --add-repository-rebuild option to branch command
+ - add service merge command
+ - add service wait command
+
+0.152
+ - add support searching for groups via "group:" prefix
+ - show possible used incident projects on "maintained" command
+ OBS 2.7 only:
+ - support buildtime source services
+ - support maintenance_incident requests with acceptinfo data
+ - support maintenance_release requests with acceptinfo data
+
+0.151
+ - fixed shell command injection via crafted _service files (CVE-2015-0778)
+ - fix times when data comes from OBS backend
+ - support updateing the link in target package for submit requests
+ - various minor bugfixes
+
+0.150
+ - support local builds using builenv (for same build environment as a former build)
+ - add "osc api --edit" option to be able to edit some meta files directly
+ - follow the request order of the api (sorting according to priorization)
+ - add mr --release-project option for kgraft updates
+ - add support for makeoriginolder in request
+
+0.149
+ - removed "--diff" option from the "createrequest" command
+ - introduced new "vc-cmd" config option, which is used to specify the path
+ to the vc script
+ - various bugfixes
+
+0.148
+ - support new history including review history of OBS 2.6
+ - display request priorities, if important or critical
+ - add "osc rq priorize" command to re-priorize existing requests
+ - allow also "osc rq ls" shortcut
+ - fish shell completion support
+
+0.147
+ - support groups in maintainership requests
+ - fixing listing of review requests
+ - support expanded package listing (when using project links)
+ - fixing "osc add git://" behaviour
+ - using xz as default compression
+ - support local debian live (image) build format
+ - handle ppc64le for debian as well
+ - fix buildlog --strip-time
+ - some more minor bugfixes
+ - speedup update of a project working copy (in some cases)
+
+0.146
+ - support maintenance release request with acceptinfo data (OBS 2.6)
+ - setlinkrev can be used to update frozen links to current revisions again
+ - report errors in case request accept fails
+ - support epoch number handling for local builds
+ - support bugowner request handling for groups
+ - support usage of fedoras mock to build packages
+ - support build --prefer-pkgs for Arch linux
+ - support bash-completion for .kiwi files
+
+0.145
+ - allow to use the set-release option when running a manual release
+ - added support for "osc requestmaintainership PROJECT"
+ - various bugfixes:
+ - print_buildlog: do not strip tabs
+ - fixed "osc -H ..." in combination with a proxy
+ - fixed creation of ~/.osc_cookiejar
+ - Package.commit: create _meta for newly added packages
+ - fixed behavior of set_link_rev #72
+
+0.144
+ - allow commiting to package sources from linked projects. osc will ask to branch it first.
+ - group support in bugowner and maintainer command
+
+0.143
+ - add option to add a auto-accept in future for delete requests (handy for admins)
+ - many bugfixes:
+ - plugin loading
+ - bugowner handling
+ - download of server side generated source "up -S"
+ - wipebinaries command
+
+0.142
+ - ppc64p7 build support
+ - request --no-devel to disable request forwarding
+#
+# Features which requires OBS 2.5
+#
+ - authentification token support
+
+0.141
+ - crash fixes
+ - support for kiwi appliance builds using obsrepositories:/ directive
+ - support for manual release of sources and binaries
+ - add --last parameter for build logs to show last finished log file, if currently building
+ - add signkey --sslcert option to fetch the optional create ssl certificate instead of gpg key
+
+0.140
+ - support python 2.7 and python 3 in parallel now
+ - reworked plugin loading mechanism in order to avoid the (mass) breakage of existing
+ plugins due to the python 3 support. Nonetheless if a plugin uses the "@cmdln.option(...)"
+ decorator it has to import the cmdln module first via "from osc import cmdln".
+ - allow specifying directories as mv targets
+ - drop the support for deprecated cbinstall and cbpreinstall directives
+ - allow to set maintainer or bugowner ship for a binary package initially, but ask back if
+ this is the right place.
+ - support listing of deleted source files "ls -D $PROJECT $PACKAGE"
+ - build results do show that a succeeded is not yet published
+ - improved bash completions
+ - default build root includes repository and architecture name now
+ - --request-accept-or-revoke option, useful to handle mass approval of requests
+ - multiple minor bugfixes
+
+0.139
+ - various bugfixes for owner search
+ - support generic emulator virtualization
+ - added "--host" argument to "osc build" (used to perform the build on a remote host)
+ - "search --maintained" is obsolete. Abort on usage.
+ - "maintainer --user" support to search for all official maintained instance for given user or group
+ - added support to abort a commit after displaying a default commit message in $EDITOR. As a result
+ other commands like "submitrequest" will also ask if the user wants to proceed if the default
+ comment/message wasn't changed.
+
+0.138
+ - add support to remove repositories recursively (mostly only usefull for admins)
+ - submitrequest: old not anymore used maintenance code got removed. It is possible now
+ to create one request to submit all changed packages of an project in
+ one request. Just run "osc sr" in the checked out project directory.
+ - disable keyring usage by default. print warning about misconfigured keyrings.
+ - prdiff: new command to diff entire projects
+
+0.137
+ - support single binary download via getbinaries command
+ - support to set the bugowner
+#
+# Features which requires OBS 2.4
+#
+ - offer to send set_bugowner request if target is not writeable
+ - support delete requests for repositories.
+ - support default maintainer/bugowner search based on binary package names
+ - support to lookup --all definitions of maintainers of bugowners. Either
+ for showing or setting them.
+ - buildinfo --debug option for verbose output of dependency calculation
+
+0.136
+ - prefer TLS v1.1 or v1.2 if available
+ - declined is considered to be an open state (that is "osc rq list" also shows declined requests)
+ - added support to move files across packages via "osc mv" (fixes issue #10)
+ - various bugfixes:
+ * show source package name when running "osc se --binary ..."
+ * fixed encoding detection
+ * fixed build result listing for arch packages (affects "osc build")
+ * "osc ci --noservice" works also for "external"/flat packages
+
+0.135.1
+ - do not forward requests to packages which do link anyway to original request target
+
+0.135
+ - request accept is offering now to forward submit request if it is a devel area like webui does
+ - support archlinux builds (requires OBS 2.4)
+ - support maintenancerequest from local checkout
+ - bugfixes for review handling, result watching, gnome-keyring
+
+0.134
+ - patchinfo call can work without checked out copy now
+ - use qemu as fallback for building not directly supported architectures
+ - "results --watch" option to watch build results until they finished building
+ - setlinkrev and linkpac ---current is setting vrev. this requires OBS 2.1.17 or 2.3
+ - security fix for buildlog function, terminal control characters are limited now.
+#
+# Features which requires OBS 2.3
+#
+ - support dryrun of branching to preview the expected result. "osc sm" is doing this now by default.
+ - maintenance requests accept package lists as source and target incidents to be merged in
+ - add "setincident" command to "request" to re-direct a maintenance request
+ - ask user to create "maintenance incident" request when submit request is failing at release project
+ - "osc my patchinfos" is showing patchinfos where any open bug is assigned to user
+ - "osc my" or "osc my work" is including assigned patchinfos
+ - "osc branch --maintenance" is creating setups for maintenance
+ - "osc unlock" command to unlock packages or projects
+
+0.133
+ - add --meta option also to "list", "cat" and "less" commands
+ - project checkout is skipping packages linking to project local packages by default
+ - add --keep-link option to copypac command
+ - source validators are not called by default anymore:
+ * They can get used via source services now
+ * Allows different validations based on the code streams
+#
+# Features which requires OBS 2.3
+#
+ - support source services using OBS project or package name
+ - support updateing _patchinfo file with new issues just by calling "osc patchinfo" again
+ - branch --add-repositories can be used to add repos from source project to target project
+ - branch --extend-package-names can be used to do mbranch like branch of a single package
+ - branch --new-package can be used to do branch from a not yet existing package (to define later submit target)
+ - show declined requests which created by user
+
+0.132
+ - rdelete and undelete command requesting now a comment
+ - add 'requestbugownership' command for setting the bugowner via request
+#
+# Features which requires OBS 2.3
+#
+ - new command "createincident" to create maintenance incidents without a request
+ - support to create hidden project on "branch" and "createincident" commands
+ - osc waits and updates package after checkin when a source service is used
+ - support for the new service file mode for "update" and "checkout" command when
+ downloading server side generated files
+ - integration for local source services, they will replace the source_validator mechanism
+
+0.131
+ - new command 'develproject' to print the devel project from the package meta.
+ - add blt and rblt commands, aka "buildlogtail" and "remotebuildlogtail" to show
+ just the end of a build log (for getting the fail reason faster).
+ CHANGE: the --start parameter is now called --offset
+ - add "createrequest -a add_group" option to create a group request
+ - add "createrequest -a add_me" shortcut
+ - add "less" command, doing the same as "osc cat" but with pager
+ - fallback to unexpanded diff mode on "osc diff" on merge error.
+ - support viewing the commit history of deleted packages
+ - show review states on "review list"
+ - new source service commands "localrun" and "disabledrun" to generate files without _service: prefix
+ - add "request supersede" and "review supersede" to supersede with existing request
+ - make it possible to run single source services, even when not specified in _service file.
+ (For example for doing a version update without creating a _service file: osc service lr update_source)
+ - protect rebuild and abortbuild commands with required "--all" option to mass failures by accident (similar to wipebinaries)
+ - "review accept/decline" is trying to change all reviews of a requests, if a specific one is not specified by user
+#
+# Features which requires OBS 2.3
+#
+ - "my requests" is doing faster and complete server side lookup now if available
+ - "review" command has been extended to handle reviews by project or by package maintainers
+ - support for new source service modes: disabled, trylocal and localonly
+ - support project wide source services
+ - support for armv7hl architecuture. used to denote armv7 + hardfloat binaries
+ - add force option to accept requests in review state.
+ - add "maintenancerequest" command to request a maintenance incident from maintenance team
+ - add "releaserequest" command run a maintenance update release process (for maintenance team only)
+ - allow to force the storage of project meta data (to ignore depending repositories for example)
+ - "my requests" is showing requests with open reviews also now
+
+0.130
+ - new "revert" command to restore the original working copy file (without
+ downloading it)
+ - rewrote "diff" logic
+ - added new "--http-full-debug" option, "--http-debug" filters the
+ "Authentication" and "Set-Cookie" header
+ - added new "--disabled-cpio-bulk-download" option: disable downloading
+ packages as cpio archive from api
+ - added new "repairwc" command which tries to repair an inconsistent working
+ copy
+ - workaround for broken urllib2 in python 2.6.5: wrong credentials lead to an
+ infinite recursion
+ - support --interactive-review option when running "osc rq list <project>"
+ - improved "osc rq show <id> --interactive-review"
+ - do_config: added new options --stdin, --prompt, --no-echo:
+ --stdin: read value from stdin
+ --prompt: prompt for a value
+ --no-echo: prompt for a value but don't echo entered characters (for
+ instance to enter a passwd)
+ - added template support for a submitrequest accept/decline message
+ - lots of internal rewrites (new working copy handling etc.)
+ - support added for osc search 'perl(Foo::Bar)'
+ - New "service" command to run source services locally or trigger a re-run on the server.
+ - setlinkrev is setting now the revision to xsrcmd5 by default to avoid later breakage on indirect links by default.
+
+ NOTE:
+ Due to the rewrite of the working copy handling osc might fail with the
+ following error:
+ Your working copy '.' is in an inconsistent state.
+ Please run 'osc repairwc .' (Note this might _remove_
+ files from the .osc/ dir). Please check the state
+ of the working copy afterwards (via 'osc status .')
+ Simply run "osc repairwc" which might fetch files from the api
+ or delete some files from the storedir (.osc/). It won't touch
+ locally modified files. For more information see section
+ "WORKING COPY INCONSISTENT" in the README.
+
+#
+# Feature which requires OBS 2.1
+#
+ - support reliable diff for an accepted request
+
+0.129
+ - "dists" command to show the configured default base repos from the server.
+ - "review list" command to list open review requests
+ - "review add" command to add another reviewer for a request (either user or group)
+ - add "buildinfo --prefer-pkgs <dir>" option
+ - add "prjresults --hide-disabled" option to hide packages which are disabled/excluded
+ in all repos and repos which have only disabled/excluded packages
+ - harmonize "api"'s options with curl's options
+ - use builtin signature check by default (instead of verifying the signature with "rpm -K...")
+ - add "status --show-excluded" to show all files (except the store dir)
+ - new "osc reqmaintainership" command which is a shortcut for
+ "osc creq -a add_role USER maintainer PROJECT PACKAGE"
+#
+# Feature which requires OBS 2.1
+#
+ - add "osc aggregate --nosources" option
+ - add "request clone" command to clone all packages from a given request
+ - fixed references into en.opensuse.org to honor the new Wiki structure
+ - add cross build targets mips and mipsel for QEMU Usermode. needs also build update.
+
+0.128
+ - better default commands selection for editor/pager
+ - support "osc rq reopen" to set a request in new state again
+ - "osc repos" and "wipebinaries" is checking for local project now
+ - "osc getbinaries" works in project dir now
+ - support added for SPARC builds
+ - support build --oldpackages
+ - introduced the "trusted projects"
+ - Fixes for default editor, api check on deleterequest call, tempfile leaks, getbinaries source package handling, results command
+#
+# Feature which require either OBS 2.1 or 2.0.4
+#
+ - add osc signkey --extend for extending the expiration date of the gpg public key
+
+0.127
+ - add size limit mode, files can be ignored on checkout or update given a certain size limit.
+ - --csv/--format options for results command - using format user can explicitly specify what he wants print
+ - osc branch reads project/package in package directory
+ - fix creation of package link, when target project has the package via linked project
+ - add "osc rq approvenew $PROJECT" command to show and accept all request in new state.
+ This makes sense esp. for projects which work with default reviewers before.
+ - support external source validator scripts before commiting
+ - support request creation with multiple actions
+#
+# Features which require OBS 2.0
+#
+ - support "osc add http://...", this uses obs source service for downloading a file and verify it via sha256 verifier service
+ - add support for CBpreinstall/CBinstall
+ - support branch --force to override target
+ - support for "unresolvable" state of OBS 2.0
+ - support undelete of project or package
+ - support for package meta data checkout
+
+0.126
+ - added VM autosetup to osc. This requires appropriate OBS version and build script version.
+ - enhanced QEMU cross build support with 'armv4l' 'armv5el' 'armv6el' 'armv7el' 'armv8el' 'mips' 'mips64' 'ppc' 'ppc64' 'sh4' arch strings now supported on x86 host
+ - suggest git, svn, ... if indicated, after oscerr.NoWorkingCopy
+ - "osc cat" & "osc ls" now auto-expands through link.
+ - fixed "osc add" after "osc delete".
+ - fix "osc patchinfo" command (crashed before)
+ - fixed SSL proxy support
+ - fixed meta attribute create and set calls
+ - osc remotebuildlog supports a buildlogurl
+ - Allow --prefer-pkgs to parse repodata
+ - new "osc build --no-service" option to skip source service update
+ - fix linktobranch apiurl usage
+ - "maintained package" search is telling relevant projects now
+ * requires OBS 1.7.2 or 2.0
+ - added "osc chroot" command
+ - fixed #547005 ("osc co could show download progress")
+ - added "--interactive" option to "osc request"
+ - store commit message so it doesn't get lost on failure
+ - added "--cpio-bulk-download" and "--download-api-only" options to "osc build"
+ - added "osc localbuildlog" command
+ - added "--build-uid uid:gid|caller" option to "osc build" to specify abuild id in chroot
+ - verify files using rpm bindings and keys supplied by buildservice
+ - added "--exclude-target-project <prj>" option to "osc rq list"
+ - added "--message" option to "osc branch"
+ - added "osc config" command to set/get/delete a config option
+ - added "--binary" and "--baseproject" options to "osc search"
+ - added "-o/--offline" and "-l/--preload" options to osc build
+ * osc build -l standard i586 foo.spec (to cache all dependencies)
+ * osc build -o standard i586 foo.spec (to build without contacting the api)
+
+0.125
+ - add "osc pull" command to fetch and merge changes in the link target
+ - new proxy support via SSL
+ - when a broken link is encountered automatically switch to last working
+ version. use 'osc pull' to repair the broken link.
+ - osc my request is showing now also requests from other people target to
+ myself
+
+ #
+ # Features which require OBS 1.7
+ #
+ - new config option 'submitrequest_on_accept_action' to specify a default action
+ if a submitrequest has been accepted
+ - add "osc linktobranch" command to convert a classic link to a branch package
+ - show scheduler state for each repo with "results" and "prjresults"
+
+0.124
+ - added 'osc bugowner' as a more intelligent version of 'osc maintainer -B'
+ - added option '-B' to osc maintainer, prints bugowner OR maintainer.
+ - added 'osc req help' as convenience alias to 'osc help req'.
+ - 'osc in' to be done. Its usage just prints a suggested zypper command line.
+ - give better hint how to use osc vc without network connectivity.
+ - added printing of cache statistices to osc build
+ - support http proxies when using python 2.6 or newer (#551004)
+ - partial fix for checkout problems (bnc#551147)
+ - fixed #477690 ("osc fetching binaries really slow")
+ - osc jobhistory accepts also "prj [pkg] repo arch" now
+ - osc buildinfo accepts now also "prj pkg repo arch [spec/dsc]"
+ - osc buildconfig accepts now also "prj pkg repo arch"
+ - fixed warning messages regarding SSL certificate on some plattforms (Fedora)
+ - support submit requests on project level, osc is checking which packages
+ have changed and submits only the changed after asking back.
+ - show worker/id on jobhistory and make it faster by adding a default limit of 20
+ - add "osc build --root" option to allow to specify build root directory
+ - add "osc build --release" option to allow to specify a package release number
+ - added osc mv command which can rename file and leave them under version control
+ - added new commands "dependson" and "whatdependson" to find out which packages get
+ triggered before checkin/request accept.
+ - add new "osc build --linksource" option, speeds up esp. image building a lot
+ - add "osc triggerreason" command to show detail reason, why a package got triggered for build
+ - Incompatible changes:
+ * osc se now prints Project Package, instead of Package Project
+ for easier copy&paste.
+ * osc se uses exact search by default. Use osc se -s for
+ substring search
+ * osc repourls neither needs nor accepts a path to a package
+ working dir anymore
+ * osc repo neither needs nor accepts a path to a package or
+ project working dir anymore
+ #
+ # Features which require OBS 1.7
+ #
+ - search: allow to limit results via existing attibutes
+ - added "osc meta attribute" for basic attribute creation, deletion, showing and value setting
+ - implement "osc mbranch" call to create projects with multiple source package (instances)
+ - new "osc patchinfo" command: basic patchinfo generation and modification support
+ - add support for _patchinfo package submissions in "osc sr" on project level
+ - support review handling of requests (new "osc review accept/decline $REQUEST_ID" command
+
+0.123
+ - IMPORTANT: ssl certificate checks are actually performed now to
+ prevent man-in-the-middle-attacks. python-m2crypto is needed to
+ make this work. Certificate checks can be turned off per server
+ via 'sslcertck = 0' in .oscrc.
+ - 'osc list' option -D now only limits non-'new' requests. In state 'new' all are shown.
+ - suggest 'osc list' --bugowner option. Not implemented.
+ - added 'osc rq help' as convenience alias to 'osc help rq'.
+ - 'osc in' to be done. Its usage just prints a suggested zypper command line.
+ - Incompatible change: osc se now prints Project Package, instead of Package Project
+ for easier copy&paste.
+ - fix checkout of packages, which contain not committed files (but uploaded)
+ - add signing key management command (osc signkey)
+ * shows public part of project key
+ * allows (re)creation of a project key
+ * allows deletion of a project key
+ - support 100% offline build when using "osc build --noinit ..."
+ -> buildinfo gets cached in local directory as .buildinfo.xml
+
+0.122
+ - added missing code for 'osc sr -l [ID]'
+ - allow osc cat with one parameter, if it is a url.
+ - make osc getpac really get the package (instead of branch only)!
+ - expanded several tabs to spaces.
+ - added default project to new getpac and bco subcommand. .oscrc:getpac_default_project = OpenSUSE:Factory
+ (not added to branch subcommand, to not interfere with its syntax.)
+ - add support for generic python-keyring lib, supports KWallet, Gnome keyring, MacOS and Windows.
+ - make buildhist command usable without checked out package
+ - rename old "platform/s" names to "repository/ies" (internal cleanup only)
+ - fixed osc diff -c N, it failed with int and string concatenation
+ - made osc diff and rdiff more similar: added -p, -c to rdiff, removed -u from rdiff.
+ made -u default for both, renamed --pretty to --plain as it is the opposite of -u
+ #
+ # Features which require OBS 1.7
+ #
+ - option to download server side generated _service:* files on update
+ - support for running source services locally. Happens by default on source update
+ and build.
+ - support modification flages on creation of submit request
+ (for auto update or clean up packages or to avoid it, when submit request got accepted)
+ - show request ids from package source logs
+ - added support to require local packages which don't exist in the obs for a local build. This
+ fixes #377021, #481193
+
+0.121.1
+ - fixed creation of new ~/.oscrc files
+ - fixed "osc my request" command
+
+0.121
+ - fixed osc rq list -U to not look into the local dir
+ - added osc my ... pkg/prj/req shorthand commands
+ - add 'osc se' alias for 'osc search -e'
+ - add -b -m -M to 'osc search'
+ - hack for _help_preprocess_cmd_option_list to survive setup.py build
+ - made rresults an alias for results. python decorators are a strange concept...
+ - asserting that ~/.oscrc remains mode 0600
+ - no more plain text passwords in ~/.oscrc, we store now as bz2+base64
+ - added verbosity control -v -q. To be used in guess_proj_pack()
+ - added 'll' and 'ls -l' as shorthand to 'list -v'
+ - started to change to explicit dual license GPLv2 or GPLv3 to conform to Novell policy.
+ - added revision parameter to show_upstream_srcmd5(), so that it can be used in do_cat later.
+ - allowed both integer and srcmd5 revisions in meta_get_filelist()
+ - added 'lL', 'LL': allowed -e and -v together in do_list(). Was an internal error before.
+ - added cat -e, to cat a file through a link.
+ 'cat -e -r 3' expands through the third revision of the _link.
+ - added subcmd bco as alias for branch -c
+ - added primitive experimental support for .oscrc:checkout_no_colon = 1
+ - suggest using svn when .svn found.
+ - alias submitpac submitrequest
+ - osc bco now continues to checkout after branch target exists error.
+ - added .oscrc:plaintext_passwd=1 for backwards compatibility
+ - moved core.py:exclude_stuff to .oscrc:exclude_glob and expand it to catch *.orig etc.
+ - added osc rq list -a; a shorthand for enumerating all states
+ - osc rq list -D nnn limit to requests nnn days old.
+ - osc sr --diff option added
+ - improved help texts with repairlink to point to osc resolved.
+ - improved passx code when creating oscrc.
+ - osc metafromspec allows editing before send
+ - allow handling of other roles than "maintainer" with maintainer command
+ (-r role)
+ - fix and improve request list and show output
+ - new osc rremove command for remote source files removal
+ - first part of support to handle _service\* files correctly
+ - osc commit asks if some file has a '?' status (can be skipped by --force option)
+ - fixed request list for multiple states
+ - new option --overlay
+ - new option --rsync-src / --rsync-dest
+
+0.120:
+ - support "setlinkrev" for whole projects
+ - add "setlinkrev --unset" for removing revision references
+ - add "osc request list -t <type>" to list only submit, delete or develchange requests
+ - add shell completion scripts
+ - fix support of listing requests with multiple actions
+ - "osc maintainer" is following to the development project / package now
+ - "osc maintainer" list maintainer and bugowner roles now
+
+0.119:
+- Support new request types
+ - "submitreq" command has a new syntax (incompatible !)
+ - new "deleterequest" command
+ - new "changedevelrequest" command
+ - new "request" command for showing/modifing requests
+ - Multiple actions in one request is not yet supported by osc
+ - The new commands require an OBS 1.7 server, submitreq is still working with
+ older servers.
+- support of added .changes in commit message template
+- make submit request listing fast by server side filtering
+- allow pulling of conflicting changes via "osc repairlink"
+- delete commands consolidated:
+ * deleteprj and deletepac are obsolete.
+ * delete and rdelete take over
+- enable package tracking by default
+- bugfix: templates in edit commit message causes an empty commit logs
+- osc submitrequest consumes DESTPRJ [DESTPKG] arguments only
+- osc build now also tested on native arm targets where uname -m reports a string like armv{4l,5el,6l,7el,7l}
+- osc rlog now works with srcmd5 also
+- plugins now should be placed in /usr/lib/osc-plugins to match FHS (the /var path is still supported though)
+- osc now includes automatically generated man page
+- osc can now store credentials in Gnome keyring if it is available
+- new support for osc linkpac to specify cicount attribute
+- new log/rlog output formats (CSV and XML)
+- new jobhistory/buildhistory/search output format (CSV)
+- new option to fetch buildlogs starting at given offset
+- new option for copypac
+ * -r to specify source revision
+ * -m to specify a comment (and send default comment if not specified)
+- new option to results(r), and rresults:
+ * -r|--repo to specify a repository(repositories)
+ * -a|--arch to specify a architexure(s)
+ * --xml for xml output (makes results_meta obsolete)
+- request list -M shows open SRs created by the user.
+- Fixed build support for images, only refered packages from buildinfo get used. (#485047)
+- "req" command got renamed to "api" to avoid clash with "request" command
+- osc build has a smarter default platform selection - it checks the
+ availibility config value, 'standard' and 'opensuse_Factory' in platforms list and in case
+ of fail it uses the last entry from that list
+- new osc linkpac -f to allow to override existing _link files
+- rename "rebuildpac" to "rebuild", but keep "rebuildpac" as alias
+
+0.117:
+- support checkout of single package via "osc co PACKAGE" when local dir is project
+- allow to specify target project and package on osc branch (requires server version 1.6)
+- add option to automatic checkout a branched package
+- support "osc getbinaries" in checkout packages
+- new vc command for editing the changes files (requires build.rpm 2009.04.17 or newest)
+- new repairlink command for repairing a broken source link (requires server version 1.6)
+- '-b|--brief' option for osc submitreq show subcommand
+- use "latest" commited revision on checkout, not "upload" (#441783)
+- '-e|--just-open' option for vc command and used /usr/lib/build/vc as an executable
+
+0.116:
+- support listings of older revisions with "osc ls -R"
+- add --current parameter for linkpac to use current revision of source package fixed.
+- add osc setlinkrev to add or update revision number in links easily
+- fix streaming of binary files via "cat" (#493325)
+
+0.115:
+- optional transfer of devel project during copy_pac and link_pac is fixing
+ opertation with remote build service instance
+- "osc ci" fails uploading large files to Provo BuildService
+- fixed support for accessing download repositories (worked only for download.o.o so far)
+
+0.114:
+- the .oscrc config handling has been cleaned up:
+ * use "apiurl" for everything now (== <protocol>://<host>)
+ * added aliases support for [apiurl] sections in the ~/.oscrc.
+ Example:
+ [http(s)://foobar]
+ ...
+ aliases = foo, bar
+ => "osc -A foo <cmd>" will do the same as "osc -A http(s)://foobar ls"
+ * "scheme" and "apisrv" are deprecated and will produce a warning
+ * when writing a new ~/.oscrc, store the apiurl in the conffile (bnc#478054)
+ * fixed bug that made osc ask for credentials when -A was used (bnc#478054)
+ * fixed crash upon password entry (first startup) (bnc#478052)
+- osc build:
+ * make product builds work
+ * speed up by using a cookie when fetching the binaries (bnc#477690)
+ * support for VM (kvm or xen) builds
+ * obsolete the need to configure download server, get it from the build
+ service instance instead.
+ * be a bit more verbose if the linked package isn't expanded (bnc#470948)
+- osc branch:
+ * --develproject option fixed (the API calls it 'ignoredevel' instead of 'nodevelproject')
+ * --revision option added
+- osc jobhistory: new command to see build job history of a project or a package
+- osc results/rresults: option -l, --last-build added (show last build results)
+- osc linkpac: fix failure when -A<url> is used (bnc#479156)
+- osc commit: don't scare users if they want to commit a nonexistent file (bnc#469167)
+- osc diff: bugfix to make --pretty option work
+- 11.1 added to the osc project template
+
+
+0.113:
+- osc diff -rX:Y: the default is to return an unified diff (to get a pretty
+ diff use the --pretty option)
+- osc rdiff: the default is to return a pretty diff (to get an unified diff use the --unified option)
+- osc sr show --diff: the default is to return a pretty diff (to get an unified diff use the --unified option)
+- osc getbinaries: optionally also download source rpms
+- osc importsrcpkg: set the url in the package meta (bnc#458083)
+- osc wipebinaries: added --expansion option
+- added support for format strings like "%(project)s" and "%(package)s" which
+ can be used in the build-root config option. For example one could use a new
+ chroot for each package.
+- osc updatepacmetafromspec: fix failure if %description is starting with newline (bnc#462869)
+- catch OSError exceptions which might be raised by the subprocess module
+- don't use a hardcoded path for the rpm binary otherwise it fails on
+ distributions like debian
+- osc meta: be more verbose in case of failure (bnc#459292)
+- osc mkpac: add info how to enable the package tracking feature (bnc#459288)
+
+0.112:
+important bugfix:
+- osc deletepac: prevent recursive deletion of a whole project [bnc#458535]
+- osc build: support more options: --icecream, --ccache, --with, --without
+- osc build: --keep-pkgs also saves the src.rpm now
+- osc build: small fix in debuginfo handling
+- osc build: new armv7el arch for all binaries for up to ARMv7 EABI with VFP
+
+
+0.111:
+- fix accidental truncation of .oscrc to 0 bytes
+- fix osc's ignorance of the revision option (-r) for expanded links
+- osc build: handle kiwi builds (local image build)
+- osc build: cross build support
+- osc build: support for ARMv5 EABI little endian arch added
+- osc build: fixed detection of the build type (rpm or deb), after change in the buildinfo
+- osc build: build debuginfo packages if enabled in the project/package meta (this partly fixes #421390)
+
+
+0.110:
+
+- osc build: no working copy needed anymore when building a local package [bnc#431434]
+- osc checkout: when checking out a project, and a linkerror occurs for one of
+ the packages, do a checkout in unexpanded form and continue checking out the
+ rest of the project [bnc#428303]
+- osc deletepac, osc branch: allow slash notation for the project/package arguments
+- fix deprecation warnings on Factory (which uses Python 2.6)
+- fix to avoid (internal) stale Package objects [bnc#436932]
+
+
+0.109:
+
+- osc getbinaries: new command to download binaries directly from the api server
+- osc rlog: new command to show commit logs of remote packages
+- osc build: --debug option to the build script which will take care of creating debuginfo packages
+- add link to plugin API to osc help output
+- avoid a hard dependency on the rpm-python bindings.
+- fixed depracation warnings with Python 2.6 [bnc#426612]
+- streaming of unfinished logfiles fixed
+- fixed regression of .oscrc template [bnc#427118]
+Changes were from Marcus_H, poeml, dmueller, tpatzig.
+
+
+0.108:
+
+- osc submitreq: has two aliases now: "osc sr" and "osc submitrequest"
+- osc sr create: prompt to revoke existing requests
+- osc sr revoke: new command for to get rid of requests to projects one can't write to
+- osc sr list: allow showing requests in a state other than "new"
+- osc sr show: show the current state's comment
+- osc sr log: new command to show the history of a given id
+- osc sr: enable requests for submitting new packages
+- osc build: implement --no-checks
+- osc build: be less strict on the arguments, and guess what's needed. For instance:
+ * osc build PLATFORM ARCH BUILD_DESCR
+ * osc build PLATFORM (ARCH = hostarch, BUILD_DESCR guessed)
+ * osc build ARCH (PLATFORM = build_platform (config option), BUILD_DESCR guessed)
+ * osc build BUILD_DESCR (PLATFORM = build_platform (config option), ARCH = hostarch)
+ * osc build (PLATFORM = build_platform (config option), ARCH = hostarch, BUILD_DESCR guessed)
+- osc build: download after the target architecture check
+- osc addremove: bugfixes, --recursive option
+- osc init: added support to initialize a project dir
+- osc metafromspec: new alias for 'updatepacmetafromspec' which is hard to remember
+- osc updatepacmetafromspec: also update URL
+- osc buildlog: do not download entire log to memory
+- new http_headers option to add arbitrary headers to HTTP requests
+- bugfix to make osc work on Gentoo
+- enhance/update the package and project template
+- .netrc heritage from previous commandline client has been removed
+- osc asks for password now, when used with -A
+
+
+0.107:
+
+- osc build: the --extra-pkgs option is now a configurable setting in .oscrc.
+ Default is "extra-pkgs = vim gdb strace"
+- .oscrc: make tilde expansion work on the packagecachedir setting
+- osc update / checkout: don't check out a working copy, or update an existing
+ one, when a source link cannot be applied [bnc#409373]
+
+
+0.106:
+
+- osc rdiff / osc submitreq show: diff the _expanded_ sources [bnc#408267]
+- osc submitreq list: show author's name
+- osc submitreq: shortcut alias 'sr' added
+
+
+0.105:
+
+- osc submitreq list:
+ - can now be called without parameters, applying to the working copy then.
+ - calling it in a project directory is also possible now.
+ - output was improved. Newest requests are listed first.
+- osc submitreq delete: a new action which has been added
+- osc submitreq list/create: use api URL from the working copy
+- osc meta: editing returns the API error description instead of a plain HTTP
+ error if available
+- osc copypac: use the correct userid when copying to another api host
+- osc importsrcpkg: disable signature check when getting data from a rpm file
+- osc linkpac: --revision option added.
+- osc search: added option -i|--involved, to show in which projects/packages
+ a developer is involved
+- osc build: double check the buildinfo for local builds. Refuse to build for
+ architectures that are not supported by the host
+- osc buildhist: change the output into a format which better matches actual
+ RPM filenames.
+- osc commit: give commit message tempfiles a ".diff" suffix, so syntax
+ highlighting automatically works in capable editors
+- other bug fixes:
+ - don't expand/unexpand if the working copy has local modifications - this is
+ an ugly workaround for #399247 but this way the working copy isn't screwed up
+ - work around a bug which causes packages to be cached locally under the
+ "None" architecture (and therefore causing issues when building for more
+ than one architecture via osc build).
+ - don't create _linkerror files in working copies
+ - better error handling (mostly printing more details) in a number of cases
+ - show error messages from the API also for type 500 errors
+
+
+0.104:
+- osc update: after update, reset the revision when updating multiple package.
+ Fixes "404: Not Found" type errors when updating an entire project. [bnc#399177]
+- more/better error messages in some error scenarios
+- osc wipebinaries: add missing check for commandline arguments, which could
+ cause a PACKAGE argument to be ignored
+- fixed make_diff in order to avoid errors when committing a new package
+ (created with mkpac)
+
+
+0.103:
+
+- osc submitreq create: simplify by make osc guess needed parameters, if
+ there is a working copy and it is a source link.
+- osc submitreq create: don't stop on packages that have a devel project
+ defined, if the submit actually comes from that project.
+- osc checkout: checkout of source links is now done in expanded form per
+ default. The new option --unexpand-link can be used to get the raw link file.
+- show the API's error message for HTTP 403 (Forbidden) replies.
+
+
+0.102:
+
+- osc branch: Show the actually created branch project name, not
+ a guessed one. Add --nodevelproject.
+- osc submitreq: look up the develproject of the target, and if
+ there is one, don't create the request, unless forced with
+ --nodevelproject.
+- make the global -d option work better under certain circumstances
+
+0.101:
+
+- add osc branch command, using the branch API call to branch a package to
+ home:poeml:branches:PRJ/PKG
+- osc commit now opens $EDITOR for commit message
+- improved error handling, when API returns HTTP status code 400 (bad request)
+- osc status: implement -q/--quiet switch
+- osc info: slightly more verbose
+- osc deletepac: allow deletion of multiple packages at once
+- make "osc meta prjconf <project> -e" work again (probably caused by r3702)
+
+
+0.100:
+
+- improved error handling (babysitter.py wrapper, oscerr.py exception classes)
+ Tracebacks are mostly suppressed now. To enable them, use
+ -t, --traceback print call trace in case of errors
+ or set traceback=1 in .oscrc.
+- other new global options for debugging:
+ --debugger jump into the debugger before executing anything
+ --post-mortem jump into the debugger in case of errors
+ -d, --debug print info useful for debugging
+- make way for more seamless osc version updates (the .osc directory in working copies
+ will have its own versioning in the future)
+- osc rprjresults and osc rresults: new commands to show remote build results
+- osc build: added --baselibs and --jobs options
+- osc copypac: added --keep-maintainers switch
+- osc maintainer: new -D/--devel-project switch
+- BUILD_DIST environment variable will be ignored (bnc#359846)
+ The following environment variables can still be used:
+ * OSC_SU_WRAPPER overrides the setting of su-wrapper.
+ * OSC_BUILD_ROOT overrides the setting of build-root.
+ * OSC_PACKAGECACHEDIR overrides the setting of packagecachedir.
+
+
+0.99+patches (interim releases, including Wed Apr 2 16:36:40 CEST 2008)
+
+- new command submitreq, to handle "submit requests" (next generation build
+ service feature). See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
+- new link handling:
+ add support for handling linked packages in expanded form. They
+ can be checked out, updated (expanding or unexpanding them),
+ and built locally.
+ Newly introduced options are:
+ * osc checkout: --expand-link
+ * osc update: --expand-link and --unexpand-link
+- new feature: package tracking. It's not enabled by default and
+ needs to be switched on with do_package_tracking=1 in .oscrc.
+ before using. See
+ http://lists.opensuse.org/opensuse-buildservice/2008-03/msg00114.html
+- prjresults: add --csv option
+- req: add option -a / --add-header to inject arbitrary request headers
+- addremove (and others): ignore _all_ dot files (the buildservice doesn't
+ handle them)
+- copypac: do a (quicker) server-side copy by default, when source and target
+ are on the same buildservice instance.
+- build:
+ - add --debuginfo
+ - add --no-verify
+ - add --local-package to build a package which doesn't exist on the server
+ - add --alternative-project to specify a project, if the current one doesn't
+ exist on the server
+ - use api url from .osc/_apiurl [#355144]
+- new command remotebuildlog
+- diff: fix #347377 (diffing too many files)
+- checkout: check for project existance beforehand
+- rdiff: new command for server-side diffs between arbitrary packages
+- cat: new command to print a file on the standard output
+- diff: reworked functionality to show newly added files, and behaving more
+ like svn when doing diff against a certain revision
+- bugfix in {link,aggregate,copy}_pac (<person> elements)
+- checkout an empty project instead of doing nothing
+- fix prjresults for newly added packages, where build status is missing
+
+
+0.99:
+
+- aggregatepac: new command, similar to linkpac. Patch from Pavol Rusnak.
+- wipebinaries: added --build-failed and --broken [#335498]
+- deleteprj: enabled this command, as the backend now supports it
+- maintainer:
+ - added --verbose option
+ - added functionality to add/remove users from a project/package
+- print the list of URL to try, when in HTTP debug mode
+- build: allow to use lbuild, a compatible replacement for build
+- do not create dirs for non-existing packages during checkout [#259711]
+
+
+0.98:
+
+- new maintainer command, to list the maintainers of a project or package
+- ls: add -b option to list binaries
+- make osc library simpler to use from external scripts
+- new importfromsrcpkg command, to import a package src.rpm from file or URL
+- new req command, to issue arbitrary requests to the API
+- initial support for commit messages (ci -m/-F)
+- implementing a log command to review the commit log
+- renamed previous "log" command to "buildlog" (short: bl)
+- new meta command, replacing editmeta, editprj, createprj,
+ editpac, createpac, edituser, pattern
+- added search support
+- show helpful xml error messages if broken metadata is uploaded
+
+
+0.97:
+- added initial revision handling:
+ - extended "osc co prj pac" to checkout a specific revision of pac
+ - extended "osc up" to update to a specific revision
+ - extended "osc diff" to diff the working copy against a
+ specific revision on the server. NOTE: comparing two
+ server-side revisions (osc diff -r 11:12) is currently
+ not supported!
+- load subcommands from /var/lib/osc-plugins/ or ~/.osc-plugins/
+- updatepacmetafromspec scans for spec files automatically. Added --specfile option to updatepacmetafromspec.
+- wipebinaries: allow to wipe all binaries of packages for which the build is disabled
+- addremove: ignore foo.rXX, foo.mine for files which are in 'C' state
+- ls: add verbose option to print extra information for packages
+- for all server-side commands, allow arguments "foo/bar" instead of "foo bar"
+- new wipebinaries and abortbuild commands, by courtesy of Marcus Huewe
+- improved metadata error condition handling (thanks to Marcus Huewe)
+- build: add --userootforbuild option
+- build: implement -x/--extra-pkgs option (passed to backend and included in buildinfo result)
+- make filling out of username in templates work again
+- don't try to delete projects, as long it is not implemented in the backend
+- use new API route for downloading binaries also in configured URLs
+- make deletepac work again
+
+
+0.96:
+- following suggestions by Christian Boltz and Michal Marek, osc now memorizes
+ where a working copy was checked out from, saving the api server url to
+ .osc/_apiurl.
+- implement 'info' subcommand
+- use new api routes in all places
+- buildhistory works again
+- copypac: implement package copy from one buildservice instance to another
+ (--to-apiurl option)
+- the results subcommand now handles multiple <working copy> arguments
+- build: implement --prefer-pkgs and --keep-pkgs option
+- applied patch from Michael Marek, fixing all places where error
+ messages were printed to stdout instead of stderr. [#239404]
+- osc is now easier to work with when using alternative API servers. The
+ configured server can be overriden with -A <url> on the commandline.
+ "apisrv" in the config takes a URL now, so the variable "scheme" which was
+ needed in addition before becomes obsolete. For backward compatibility, a
+ hostname (and scheme variable) are accepted like before. Likewise, the auth
+ sections in the config take a URL now, or a hostname:port to keep old config
+ working. HTTP or HTTPS scheme is determined from the URL. Credentials must be
+ configured in .oscrc.
+- build: use actual api server in urllist for downloading, instead of hardcoded
+ api.opensuse.org [#265211].
+
+
+0.95:
+- rewrite the internal HTTP handling
+ - save and reuse HTTP server cookies, which can speed up HTTP requests up about
+ 5 times in an iChain setup
+ - adding http_GET/POST/PUT/DELETE() functions, which dispatch to
+ http_request(), and use them everywhere
+ - removing othermethods.py
+ - keeping urlopen(), in case it is used from externally, but have it print out
+ a "depracated" message
+ - finally, global option -H enables HTTP traffic debugging
+- implement "rebuild all failed packages", via --failed option in rebuildpac
+ subcommand
+- status -v shows all files, including unmodified ones
+- suppress the legend in prjresults by default (show with -l)
+- --version shows the program version number
+- fix the commit subcommand's argument handling. The following works correctly
+ now: osc ci ../test/onlyinwc `pwd` fstab ../test/f2
+- fix the download progress meter to work with small terminals [#266989]
+- update: when updating multiple packages, print each package name
+- make 'results' subcommand many times faster, by making only a single request
+- prjresults: sort package names
+- build: run with --norootforbuild, thereby defaulting to build as abuild user
+- build: fix (harmless) errors showing up in the build log during buildsystem
+ setup, by using the new <bdep> preinstall and runscripts attributes
+- update: when updating, don't delete files with local modifications
+- let the diff subcommand return 1 if differences were found
+- fix important bug, which could lead to overwriting local modifications when
+ upstream changes are merged in
+- if a merge fails, the store copy must be updated neverthelesss
+- fix testsuite and add testcase for successful merging
+- sort output of 'status' (unknown files first, filenames alphabetically)
+- core: added class "metadata" (merge from Susannes /branches/froh/reponator/)
+- added command alias 'stat' for 'status', like in svn
+- improved documentation/examples (Lars + Susanne)
+- print usage info if 'co' is called without arguments
+
+0.9:
+- "iChain-ready" (works with API server now using iChain authentication)
+- add runtime check for build.rpm version, so the rpm package dependency is
+ no longer required
+- add 'edituser' command for editing the metadata of a user account. It tries
+ to create a user if it doesn't exist yet. A new command 'usermeta' replaces
+ 'id' respectively 'userid'.
+- rewrite configuration handling. Now the API server can be set in .oscrc
+- ignore '.gitignore', '.pc', '*~' (now using filename matching [#208969]
+- fix 'status' to work with project directories as arguments
+- fix 'status <filename>'
+- 'rebuildpac' now accepts additional repo and arch argument. Note:
+ the syntax has changed.
+- add 'prjresults' command to display aggregated build status over
+ an entire project
+- add 'deleteprj' command (the API server doesn't seem to support
+ it yet, though)
+- change 'buildhistory' to display human-readable text
+- add 'copypac' subcommand, to copy a complete package to a new package, possibly cross-project
+- don't die if user tries to 'add' a file which is already versioned
+- don't die if 'addremove' encounters directories
+- urlopen(): for server return code 500, print out the reply body
+
+0.8:
+- build: use configuration from *local* specfile (e.g. BuildRequires)
+- build: let envvars OSC_SU_WRAPPER and OSC_BUILD_ROOT override config
+- build: allow 'dynamical' build-root setting by using %(repo)s and %(arch)s
+- add 'createpac/editpac' and 'createprj/editprj' subcommands which
+ are similar to 'editmeta' but should be more logical to find
+- added 'deletepac' subcommand
+- added 'buildhistory' subcommand (formerly 'history'). This only
+ gives out raw xml at this time
+- added 'linkpac' subcommand
+- added ".git" to the excluded files
+- adapt to API changes
+- fixed issue with uploading files when an intercepting web proxy was
+ in between osc and the api server
+- fixed creation of new packages/projects
+
+0.7:
+- initial support for local builds (subcommand 'build')
+- better error handling
+- new subcommands buildconfig, buildinfo, repos
+- remove requirement on pyxml package
+- editmeta: add examples for package/project templates
+- add support for streaming the build log (thanks to Christoph Thiel)
+- add 'rebuildpac' subcommand
+- add 'repourls' subcommand
+- don't diff binary files
+- don't try to merge binary files
+- add a preliminary 'updatepacmetafromspec' subcommand, which takes package
+ metadata from a specfile
+- fix profiling wrapper
+- set User-agent
+- bugfixes:
+ - fix handling of filenames with '+' signs
+ - make 'resolved' more robust
+ - fix merge on 'update' if called from another directory
+ - display reason for build status is 'broken'
+ - handle HTTP error codes != 404 when reading metadata in edit_meta()
+ - handle 'project not found' error in show_project_meta()
+
+
+0.6:
+- diff bugfix: sometimes displayed diff against obsolete files
+- update bugfixes: fix update of working copy when adding a file from upstream
+ which is missing locally; fix update in directory with unmodified files:
+ don't try to merge if upstream file wasn't changed at all
+- add: make it faster
+
+
+0.5:
+- help :-)
+- add 'editmeta' subcommand: Edit project/package meta information, creating
+ new project or package if it doesn't exist. The user interface is $EDITOR
+- fix status letter for files merged on update (in analogy to svn , it is
+ either G or U)
+- if an old _files listing without any metadata is found, don't bother the user
+ with it
+- make all subcommands properly importable functions
+- bug in 'resolved' command fixed, which wouldn't clear the conflict state of a file
+
+
+0.4:
+- allow 'up' inside a project directory (will automatically pull in all new
+ packages). (For past checkouts, you may need to put the project name into
+ $prjdir/.osc/_project yourself).
+- checkout: preserve mtimes
+- add diff3 merge support. Locally modified files are merged with upstream changes
+ if possible, and go into Conflict state if that fails.
+- add 'resolved' command to be used after manual merging.
+
+
+0.3:
+- use the new file metadata, which provides checksum, size and mtime
+- faster 'status', 'update', 'diff'
+- improve argument handling, now e.g. 'osc up *' is possible
+- on first usage, ask for username and password and store them in .oscrc
+ (.netrc can still be used)
+
diff --git a/PROJ_PACK.txt b/PROJ_PACK.txt
new file mode 100644
index 0000000..591676e
--- /dev/null
+++ b/PROJ_PACK.txt
@@ -0,0 +1,92 @@
+ jw, Tue Oct 20 22:09:16 CEST 2009
+
+This is a feature suggestion for easier osc commandline handling.
+Many commands require specifying Project and/or Package names.
+
+The current situation is not satisfying for the following reasons:
+ - inconsistent defaults. Some osc subcommands can take project
+ and/or package names from the current directory, if run inside a checkout
+ tree. If both project and package can use this default or only one, and if
+ one, which, depends on the command. Users have a hard time memorizing
+ which is which.
+ Examples as of osc version 0.123:
+ osc maintainer PRJ [PKG]
+ - does not look in the current directory.
+ - need at least PRJ.
+ osc list [PRJ [PKG]]
+ - Never looks at the current directory.
+ - lists all projects, if run without parameters.
+ osc checkout [PRJ] PKG
+ osc checkout PRJ
+ - takes project from current directory, if inside a checkout tree
+ - else operates on an entire project.
+ osc checkin [ARG]
+ - defaults to current project and package,
+ - if arg is a subdirectory, project is taken from current directory
+ - if arg is a file, both project and package are taken from current
+ directory.
+ osc results [PRJ PKG]
+ - takes either both or none from current directory.
+ - many commands do not look into the current directory,
+ they are cumbersome to use.
+ - sometimes PRJ/PKG can be used instead of PRJ PKG
+
+
+Suggested solution
+------------------
+
+Instead of tuning (maybe optional) positional parameters.
+We suggest to deprecate this syntax over time and instead favour an alternate
+syntax:
+ osc CMD ... [--prj PRJ] [--pkg PKG] ...
+ osc CMD ... [--proj PRJ] [--pack PKG] ...
+ osc CMD ... [--project PRJ] [--package PKG] ...
+
+These six options are new to osc, currently no existing command uses
+them. Thus the new syntax is conflict free wit the old syntax, both can be
+used in parallel.
+
+--prj, --proj, --project are synonyms.
+--pkg, --pack, --package are synonyms.
+
+osc shall support aliases, to save typing. Some implicit aliases exist,
+with well defined magic effects. Aliases substitution is literal.
+They can replace options including their parameters, or just the option, or
+just the parameters.
+ - (a dash) expands to --prj openSUSE:Factory
+ (or --prj followed by any other project as defined in
+ ~/.oscrc:default_project )
+ --prj - is synonymous to just -, for consistency.
+
+ . (a dot) evaluates the current working directory, searching for
+ .osc/_apiurl, .osc/_project, and .osc/_package
+ Implicit --apiurl, --prj, or --pkg options are constructed as far
+ as available from the current directory and as far as not already
+ present in the command line.
+ If a dot is used as parameter to an option, it has a more
+ deterministic meaning.
+ --apiurl . Substitute only the current apiurl,
+ --prj . Substitute the current project name, and provides
+ a default for --apiurl unless given.
+ --pkg . Substitures current package name likewise.
+
+ ./. expands to --prj . --pkg .
+ ./PKG expands to --prj . --pkg PKG
+
+Unless otherwise noted in the online help, magic aliases are only attempted onceper commandline, and will only apply to their respective options.
+E.g. osc ci -m - will use a simple '-' as check in messages, and the absence of any project or package will default to the current project or package, just as
+osc ci . -m - would do.
+
+Additionally, user defined aliases can be added to ~/.oscrc
+If an alias expansion has effect on the command line, the expanded line is
+printed as debug output.
+
+online help of osc commands shall refer to the above syntax like this:
+
+ osc CMD ... PROJ/PACK
+
+An additional help entry
+
+ osc help 'PROJ/PACK'
+
+shall explain the relevant details as presented herein.
diff --git a/README b/README
new file mode 100644
index 0000000..7b1e344
--- /dev/null
+++ b/README
@@ -0,0 +1,271 @@
+osc -- opensuse-commander with svn like handling
+
+
+Patches can be submitted via
+ * mail to opensuse-buildservice@opensuse.org
+ * Bugzilla: https://bugzilla.novell.com/enter_bug.cgi?product=openSUSE.org&component=BuildService
+ * or the official Git repository on Github:
+ https://github.com/openSUSE/osc
+
+
+INSTALLATION:
+
+RPM packages are here (rpm-md repository):
+http://download.opensuse.org/repositories/openSUSE:/Tools/
+
+To install from git, do
+
+ python setup.py build
+ python setup.py install
+ # create a symlink 'osc' in your path pointing to osc.py.
+ ln -s osc-wrapper.py /usr/bin/osc
+
+Alternatively, you can directly use osc-wrapper.py from the source dir
+(which is easier if you develop on osc).
+
+
+The program needs the cElementTree python module installed. On SUSE, the
+respective package is called python-elementtree (before 10.2: python-xml).
+For local building, you will need python-urlgrabber in addition. Those are
+standard package on SUSE Linux since a while. If your version is too old, you
+can find python-elementtree and python-urlgrabber here:
+http://download.opensuse.org/repositories/devel:/languages:/python/
+
+
+
+CONFIGURATION:
+
+When you use it for the first time, it will ask you for your username and
+password, and store it in ~/.oscrc.
+
+
+CONFIGURATION MIGRATION (only affects versions >= 0.114):
+
+Version 0.114 got some cleanups for the configfile handling and therefore some
+options are now deprecated, namely:
+* apisrv
+* scheme
+
+One new option was added:
+* apiurl = <protocol>://<somehost> # use this as the default apiurl. If this
+option isn't specified the default (https://api.opensuse.org) is used.
+
+So far osc still has some backward compatibility for these options but it might
+get removed in the future that's why it issues a deprecation warning in case
+one of those options is still in use.
+
+The new configuration scheme looks like the following:
+ # entry for an apiurl
+ [<protocol>://<apiurl>]
+ user = <username>
+ password = <password>
+ ...
+
+'''Before starting the migration please save your ~/.oscrc file!'''
+
+If the migration doesn't work for whatever reason feel free to send me an email
+or ask on the opensuse-buildservice mailinglist or in the #opensuse-buildservice
+irc channel.
+
+=== Migration case I (apisrv only) ===
+The apisrv option is used to specify the default apihost. If apisrv isn't
+specified at all the default ("api.opensuse.org") is used.
+The current [general] section looks like this:
+ [general]
+ ...
+ apisrv = <somehost>
+ # or
+ apisrv = <protocol>://<somehost>
+
+apisrv got superseded by the new apiurl option which looks like this:
+ [general]
+ ...
+ apiurl = <protocol>://<somehost>
+
+If apisrv has no "<protocol>" https is used. Make sure all apiurl sections have
+the new format which is described above. Afterwards apisrv can be removed.
+
+=== Migration case II (scheme only) ===
+The current [general] section looks like this:
+ [general]
+ ...
+ scheme = <protocol>
+
+This means every apiurl section which don't have the new format which is
+described above for instance
+ [<somehost>]
+ user = <username>
+ password = <password>
+ ...
+
+has to be converted to
+ [<protocol>://<somehost>]
+ user = <username>
+ password = <password>
+ ...
+
+Afterwards the scheme option can be removed from the [general] section (it
+might be the case that some sections already have the correct format).
+
+=== Migration case III (apisrv and scheme) ===
+The current [general] section looks like this:
+ [general]
+ ...
+ apisrv = <somehost>
+ scheme = <protocol>
+
+Both options can be removed if all apiurl sections have the new format which is
+described above. So basically just adjust all apiurl sections (it might be the
+case that some sections already have the correct format).
+
+
+KEYRING USAGE
+
+Osc now can store passwords in keyrings instead of ~/.oscrc. To use it,
+you need python-keyring and either python-keyring-kde or -gnome.
+
+If you want to switch to using a keyring you need to delete apiurl section
+from ~/.oscrc and you will be asked for credentials again, which will be then
+stored in the keyring application.
+
+
+WORKING COPY INCONSISTENT (only affects version >= 0.130)
+
+osc's working copy handling was rewritten in 0.130. Thus some
+consistency checks were added. As a result osc might complain
+that some old working copies are in an inconsistent state:
+ Your working copy '.' is in an inconsistent state.
+ Please run 'osc repairwc .' (Note this might _remove_
+ files from the .osc/ dir). Please check the state
+ of the working copy afterwards (via 'osc status .')
+To fix this simply run "osc repairwc ." as suggested in the
+error message. Note that "osc repairwc ." might need to contact
+the api in order to fetch some missing files. Also it might remove
+some files from the storedir (.osc/) but it won't touch any locally
+modified files.
+If it DOES NOT fix the problem please create a bug report and attach
+your working copy to the bug (if possible).
+
+
+USAGE EXAMPLES:
+(online at http://en.opensuse.org/openSUSE:OSC )
+
+To list existing content on the server
+ osc ls # list projects
+ osc ls Apache # list packages in a project
+ osc ls Apache subversion # list files of package of a project
+
+Check out content
+ osc co Apache # entire project
+ osc co Apache subversion # a package
+ osc co Apache subversion foo # single file
+
+Update a working copy
+ osc up
+ osc up [pac_dir] # update a single package by its path
+ osc up * # from within a project dir, update all packages
+ osc up # from within a project dir, update all packages
+ # AND check out all newly added packages
+
+If an update can't be merged automatically, a file is in 'C' (conflict)
+state, and conflicts are marked with special <<<<<<< and >>>>>>> lines.
+After manually resolving the problem, use
+ osc resolved foo
+
+Upload change content
+ osc ci # current dir
+ osc ci <dir>
+ osc ci file1 file2 ...
+
+Show the status (which files have been changed locally)
+ osc st
+ osc st <directory>
+ osc st file1 file2 ...
+
+Mark files to be added or removed on the next 'checkin'
+ osc add file1 file2 ...
+ osc rm file1 file2 ...
+
+Adds all new files in local copy and removes all disappeared files.
+ osc addremove
+
+Generates a diff, to view the changes
+ osc diff # current dir
+ osc diff file1 file2 ...
+
+Shows the build results of the package
+ osc results
+ osc results [repository]
+
+Shows the log file of a package (you need to be inside a package directory)
+ osc log <repository> <arch>
+
+Shows the URLs of .repo files which are packages sources for Yum/YaST/smart
+ osc repourls [dir]
+
+Triggers a package rebuild for all repositories/architectures of a package
+ osc rebuildpac [dir]
+
+Shows available repository/build targets
+ osc repository
+
+Shows the configured repository/build targets of a project
+ osc repository <project>
+
+Shows meta information
+ osc meta Apache
+ osc meta Apache subversion
+ osc id username
+
+Edit meta information
+(Creates new package/project if it doesn't exist)
+ osc editmeta Apache
+ osc editmeta Apache subversion
+
+Update package meta data with metadata taken from spec file
+ osc updatepacmetafromspec <dir>
+
+
+There are other commands, which you may not need (they may be useful in scripts):
+ osc repos
+ osc buildconfig
+ osc buildinfo
+
+
+Locally build a package (see 'osc help build' for more info):
+ osc build <repo> <arch> specfile [--clean|--noinit]
+
+
+Update a package to a different sources (directory foo_package_source):
+ cp -a foo_package_source foo; cd foo; osc init <prj> <pac>; osc addremove; osc ci; cd $OLDPWD; rm -r foo
+
+
+
+HINT FOR W3M USERS
+
+Putting the following in the file ~/.w3m/passwd will make
+w3m know the credentials for the buildservice servers:
+
+"""
+host api.opensuse.org
+ port 80
+ realm Authentication required
+ login foo
+ password bar
+
+host build.opensuse.org
+ port 80
+ realm openSUSE Build Service
+ login foo
+ password bar
+"""
+
+chmod 0600 ~/.w3m/passwd
+
+
+NOTES about the testsuite
+
+A new test suite has been created and should run via doing
+# cd tests
+# python suite.py
+
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..84c3f14
--- /dev/null
+++ b/TODO
@@ -0,0 +1,87 @@
+FIXME:
+ - more command inconsistencies:
+ osc request show
+ -B, --bugowner also show requests about packages where I am bugowner
+ osc my
+ -b, --bugowner restrict listing to items where the user is bugowner
+ osc list
+ -b, --binaries list built binaries instead of sources
+ osc search
+ -B PROJECT, --baseproject=PROJECT
+ --binary search binary packages
+ -b, --bugowner as -i, but only bugowner
+
+ osc checkout
+ -c, --current-dir place PACKAGE folder in the current directory instead
+ of a PROJECT/PACKAGE directory
+ osc branch
+ -c, --checkout Checkout branched package afterwards ('osc bco' is a
+ shorthand for this option)
+ # that means the branch checkout to cwd is not possible
+
+
+
+CRITICAL:
+ - webpage can create a _link in a fully populated package.
+ Need to prevent his somehow.
+
+ - canonical option parser.
+ -A, -e, -u, -E <n>, should be univeral to all subconmmands that work on prj/pkg objects.
+ With all subcommands that work on prj/pkg, the following should all be synonyms:
+ -A apiurl prj pkg
+ -A apiurl --project prj --package=pkg
+ -A apiurl prj/pkg
+ -A apiurl prj:pkg
+ apiurl/source/prj/pkg
+ The current working directory or its descendants should provide defaults
+ for apiurl, prj and/or pkg.
+ See also http://en.opensuse.org/openSUSE:Build_Service_Concept_OscProjPack
+
+MAJOR:
+
+NORMAL:
+
+ - split functionality that needs prj/pac as commandline arguments into a seperate tool (oscremote? osc -r?)
+ (update: we have some commands meanwhile which exist in an alternate form,
+ prefixed with r, which works remotely. E.g. rbuildlog, rprjresults, rresults)
+ - status: implement -u option as in svn [3]
+ - implement (svn-like) switch command
+ - implement 'mv' command
+ - commit: check if errors during PUT are handled sensibly, so the change is
+ not committed to localmeta
+ - add switch to commit to change repository options, like to e.g. disable publishing?
+ - implement optional logging to .osc/log, which could be useful for debugging bugs like
+ the one where api.opensuse.org sends empty replies (a hard-to-catch one)
+
+
+MINOR:
+
+ - osc checkout should display file download progress (bnc#442115)
+ - adjust zsh completion to work with cmdln.py implementation
+ - add support for adding tags to packages?
+
+
+
+
+JW:
+FIXME: osc bco ignores --nodevelproject ??
+FIXME: osc co overwrites local changes without warning.
+FIXME: when branching, the user should be added to bugowner, for the branch project.
+FIXME: 'osc rq' shall default to 'osc rq list -M -B -s all',
+ where -B shows requests related to packages where I am the bugowner.
+FIXME: 'osc log openSUSE:Factory PKG' should also point to the bsdevelproject
+
+osc addrepo - obsolete zypper ar
+ => hm, addrepo could be used also to add a repo to a project. These functionalities
+ should not conflict
+osc install - obsolete zypper in
+ -
+
+- german umlaut characters äöü do not work in the message for osc submitpac.
+ 404 not found, and no request sent.
+- implement fedora style 'osc mock' - this requires anonymous read-only access to the build server.
+ this could use http://tmp.vuntz.net/opensuse-packages/browse.py?project=openSUSE:Factory
+ as a hacky solution, while we are waiting on fate#306192
+ => we will not make rpm downloads anonymous possible, this would create too high load on the server.
+ Please improve build script instead.
+
diff --git a/dist/complete.csh b/dist/complete.csh
new file mode 100644
index 0000000..0230502
--- /dev/null
+++ b/dist/complete.csh
@@ -0,0 +1,14 @@
+onintr -
+if (! $?prompt || ! $?tcsh) goto end
+if ($tcsh == 1) goto end
+set rev=$tcsh:r
+set rel=$rev:e
+set pat=$tcsh:e
+set rev=$rev:r
+if ($rev > 5 && $rel > 1) then
+ if ( -s /usr/share/osc/complete ) complete osc 'p@*@`\/usr/share/osc/complete`@'
+ if ( -s /usr/lib64/osc/complete ) complete osc 'p@*@`\/usr/lib64/osc/complete`@'
+ if ( -s /usr/lib/osc/complete ) complete osc 'p@*@`\/usr/lib/osc/complete`@'
+endif
+end:
+ onintr
diff --git a/dist/complete.sh b/dist/complete.sh
new file mode 100644
index 0000000..6148b40
--- /dev/null
+++ b/dist/complete.sh
@@ -0,0 +1,6 @@
+test -z "$BASH_VERSION" && return
+complete -o default _nullcommand >/dev/null 2>&1 || return
+complete -r _nullcommand >/dev/null 2>&1 || return
+test -s /usr/share/osc/complete && complete -o default -C /usr/share/osc/complete osc
+test -s /usr/lib64/osc/complete && complete -o default -C /usr/lib64/osc/complete osc
+test -s /usr/lib/osc/complete && complete -o default -C /usr/lib/osc/complete osc
diff --git a/dist/osc.complete b/dist/osc.complete
new file mode 100755
index 0000000..903d7c5
--- /dev/null
+++ b/dist/osc.complete
@@ -0,0 +1,1826 @@
+#!/bin/bash
+#
+# Helper script for completion, usage with tcsh:
+#
+# complete osc 'p@*@`\osc.complete`@'
+#
+# usage with bash
+#
+# complete -C osc.complete osc
+#
+# Author: Werner Fink <werner@suse.de>
+#
+
+## For debugging only:
+## Choose your terminal not identical with the test terminal
+## exec 2>/dev/pts/9
+## set -x
+
+set -o noclobber
+shopt -s extglob
+typeset -i last
+typeset -i off
+typeset -i count
+typeset -i offset
+typeset -i remove
+typeset -i colon
+typeset -r OIFS="$IFS"
+
+if test "/proc/$PPID/exe" -ef /bin/tcsh ; then
+ export COMP_TYPE=63
+ export COMP_KEY=9
+ export COMP_LINE="${COMMAND_LINE}"
+ export COMP_POINT="${#COMMAND_LINE}"
+ let colon=0
+else
+ COMMAND_LINE="${COMP_LINE:0:$COMP_POINT}"
+ let colon=0
+ case "$COMP_WORDBREAKS" in
+ *:*) let colon=1
+ esac
+ [[ $COMMAND_LINE =~ \\: ]] && COMMAND_LINE="${COMMAND_LINE//\\:/:}"
+fi
+IFS="${IFS}="
+cmdline=($COMMAND_LINE)
+IFS="$OIFS"
+case "${cmdline[0]}" in
+iosc|isc|osc) ;;
+*) exit 1
+esac
+
+let last=${#COMMAND_LINE}
+let last--
+let count=${#cmdline[@]}
+let count--
+test "${COMMAND_LINE:$last}" = " " && let count++
+unset last
+
+oscopts=(--version --help --debugger --post-mortem --traceback --http-full-debug
+ --debug --apiurl -A --config -c --no-keyring --no-gnome-keyring --verbose --quiet)
+osccmds=(abortbuild add addremove aggregatepac api ar bco bl blt branch branchco
+ bsdevelproject bse bugowner build buildconfig buildhist buildhistory buildinfo
+ buildlog buildlogtail cat changedevelreq changedevelrequest checkin checkout
+ chroot ci co commit config copypac cr createincident createrequest creq del
+ delete deletereq deleterequest dependson detachbranch develproject di diff
+ distributions dists dr dropreq droprequest getbinaries getpac help importsrcpkg
+ info init jobhist jobhistory lbl ldiff less linkdiff linkpac linktobranch list
+ LL localbuildlog log ls maintained maintainer maintenancerequest man mbranch
+ meta metafromspec mkpac mr mv my patchinfo pdiff platforms pr prdiff prjresults
+ projdiff projectdiff pull r rbl rblt rbuildlog rbuildlogtail rdelete rdiff
+ rebuild rebuildpac releaserequest remotebuildlog remotebuildlogtail remove
+ repairlink repairwc repos repositories repourls reqbs reqbugownership
+ reqmaintainership reqms request requestbugownership requestmaintainership
+ resolved results revert review rm rq rremove se search service setlinkrev
+ signkey sm sr st status submitpac submitreq submitrequest tr triggerreason
+ undelete unlock up update updatepacmetafromspec user vc whatdependson who whois
+ wipebinaries)
+oscreq=(list log show accept decline revoke reopen setincident supersede approvenew
+ checkout clone)
+oscrev=(show list add accept decline reopen supersede)
+oscmy=(work pkg prj rq sr)
+
+oscprj=""
+oscpkg=""
+lnkprj=""
+lnkpkg=""
+apiurl=""
+alias=""
+test -s ${PWD}/.osc/_project && read -t 1 oscprj < ${PWD}/.osc/_project
+test -s ${PWD}/.osc/_package && read -t 1 oscpkg < ${PWD}/.osc/_package
+if test -s ${PWD}/.osc/_files ; then
+ lnkprj=$(command sed -rn '/<linkinfo/{s@.*[[:blank:]]project="([^"]+)".*@\1@p;}' < ${PWD}/.osc/_files)
+ lnkpkg=$(command sed -rn '/<linkinfo/{s@.*[[:blank:]]package="([^"]+)".*@\1@p;}' < ${PWD}/.osc/_files)
+fi
+if test -s ${PWD}/.osc/_apiurl ; then
+ read apiurl < ${PWD}/.osc/_apiurl
+ alias=$(sed -rn '\@^\['${apiurl}'@,\@=@{\@^aliases=@{s@[^=]+=([^,]+),.*@\1@p};}' < ~/.oscrc 2> /dev/null)
+fi
+if test "${cmdline[0]}" = isc ; then
+ alias=internal
+fi
+
+projects=~/.osc.projects
+command=osc
+
+case "${cmdline[1]}" in
+-A|--apiurl)
+ if test -n "${cmdline[2]}" -a -s ~/.oscrc ; then
+ hints=($(sed -rn '/^(aliases=|\[http)/{s/,/ /g;s/(aliases=|\[|\])//gp}' < ~/.oscrc 2> /dev/null))
+ for h in ${hints[@]} ; do
+ case "$h" in
+ http*)
+ tmp=$(sed -rn '\@^\['${h}'@,\@=@{\@^aliases=@{s@[^=]+=([^,]+),.*@\1@p};}' < ~/.oscrc 2> /dev/null)
+ if test "${cmdline[2]}" = "$h" ; then
+ alias=$tmp
+ break
+ fi
+ ;;
+ *)
+ if test "${cmdline[2]}" = "$h" ; then
+ alias=$h
+ break
+ fi
+ esac
+ done
+ fi
+esac
+
+if test -n "$alias" ; then
+ projects="${projects}.${alias}"
+ command="$command -A $alias"
+fi
+
+if test -s "${projects}" ; then
+ typeset -i ctime=$(command date -d "$(command stat -c '%z' ${projects})" +'%s')
+ typeset -i now=$(command date -d now +'%s')
+ if ((now - ctime > 86400)) ; then
+ if tmp=$(mktemp ${projects}.XXXXXX) ; then
+ command ${command} ls / >| $tmp
+ mv -uf $tmp ${projects}
+ fi
+ fi
+else
+ command ${command} ls / >| "${projects}"
+fi
+
+projects ()
+{
+ local -a list
+ local -a argv
+ local -i argc=0
+ local arg cur
+ for arg; do
+ if test $arg == "--" ; then
+ let argc++
+ break
+ fi
+ argv[argc++]=$arg
+ done
+ shift $argc
+ cur="$1"
+ if test -n "${cur}" ; then
+ list=($(command grep -E "^${cur}" ${projects}))
+ else
+ list=($(command cat ${projects}))
+ fi
+ if ((colon)) ; then
+ local colon_word
+ colon_word=${cur%${cur##*:}}
+ builtin compgen -W "${list[*]}" -- "${cur}" | sed -r "s@^${colon_word}@@g"
+ else
+ builtin compgen -W "${list[*]}" -- "${cur}"
+ fi
+}
+
+packages ()
+{
+ local -a list
+ local -a argv
+ local -i argc=0
+ local arg cur
+ for arg; do
+ if test $arg == "--" ; then
+ let argc++
+ break
+ fi
+ argv[argc++]=$arg
+ done
+ shift $argc
+ cur="$1"
+ if test -n "${cur}" ; then
+ list=($(command ${command} ls ${argv[@]}|command grep -E "^${cur}"))
+ else
+ list=($(command ${command} ls ${argv[@]}))
+ fi
+ builtin compgen -W "${list[*]}" -- "${cur}"
+}
+
+repositories ()
+{
+ local -a list
+ local -a argv
+ local -i argc=0
+ local arg
+ for arg; do
+ if test $arg == "--" ; then
+ let argc++
+ break
+ fi
+ argv[argc++]=$arg
+ done
+ shift $argc
+ if test -n "$1" ; then
+ list=($(command ${command} meta prj ${argv[@]}|\
+ command sed -rn '/<repository/{s@^\s*<.*name="([^"]*)".*@\1@p}'|\
+ command sort -u|command grep -E "^$1"))
+ else
+ list=($(command ${command} meta prj ${argv[@]}|\
+ command sed -rn '/<repository/{s@^\s*<.*name="([^"]*)".*@\1@p}'|\
+ command sort -u))
+ fi
+ builtin compgen -W "${list[*]}" -- ${1+"$@"}
+}
+
+architectures ()
+{
+ local -a list
+ local -a argv
+ local -i argc=0
+ local arg
+ for arg; do
+ if test $arg == "--" ; then
+ let argc++
+ break
+ fi
+ argv[argc++]=$arg
+ done
+ shift $argc
+ if test -n "$1" ; then
+ list=($(command ${command} meta prj ${argv[@]}|\
+ command sed -rn '/<arch>/{s@^\s*<arch>(.*)</arch>@\1@p}'|\
+ command sort -u|command grep -E "^$1"))
+ else
+ list=($(command ${command} meta prj ${argv[@]}|\
+ command sed -rn '/<arch>/{s@^\s*<arch>(.*)</arch>@\1@p}'|\
+ command sort -u))
+ fi
+ builtin compgen -W "${list[*]}" -- ${1+"$@"}
+}
+
+targets ()
+{
+ local -a targets=()
+ local -a argv
+ local -i argc=0
+ local arg
+ for arg; do
+ if test $arg == "--" ; then
+ let argc++
+ break
+ fi
+ argv[argc++]=$arg
+ done
+ shift $argc
+ let argc=0
+ for arg in $(builtin compgen -o filenames -o bashdefault -f -X '.osc' -- ${1+"$@"}); do
+ test -d $arg && targets[argc]=$arg/ || targets[argc]=$arg
+ let argc++
+ done
+ builtin compgen -W "${argv[*]}${targets+ ${targets[*]}}" -- ${1+"$@"}
+}
+
+users ()
+{
+ if test -s ${projects} ; then
+ command sed -rn "/^home:$1/{ s/^home:([^:]*):.*/\1/p}" ${projects}|command sort -u
+ elif test -s ~/.oscrc; then
+ command sed -rn '/^(user=)/{s/(user=)//p}' ~/.oscrc|command sort -u
+ else
+ command id -un
+ fi
+}
+
+submit ()
+{
+ local -i pos=$1
+ local target
+
+ if ((pos == 1)) ; then
+ if test -n "${oscprj}" -a -z "${cmdline[2]}" ; then
+ builtin compgen -W "${oscprj}" -- "${cmdline[2]}"
+ else
+ if [[ -n "${oscprj}" && "${oscprj}" =~ "${cmdline[2]}" ]] ; then
+ builtin compgen -W "${oscprj}" -- "${cmdline[2]}"
+ else
+ projects -- "${cmdline[2]}"
+ fi
+ fi
+ elif ((pos == 2)) ; then
+ if test -n "${oscpkg}" -a -z "${cmdline[3]}" ; then
+ builtin compgen -W "${oscpkg}" -- "${cmdline[3]}"
+ else
+ if [[ -n "${oscpkg}" && "${oscpkg}" =~ "${cmdline[3]}" ]] ; then
+ builtin compgen -W "${oscpkg}" -- "${cmdline[3]}"
+ else
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ fi
+ fi
+ elif ((pos == 3)) ; then
+ if test -n "${lnkprj}" -a -z "${cmdline[4]}" ; then
+ builtin compgen -W "${lnkprj}" -- "${cmdline[4]}"
+ else
+ projects -- "${cmdline[4]}"
+ fi
+ elif ((pos == 4)) ; then
+ target="${lnkpkg}"
+ target="${target:+$target }$oscpkg"
+ if test -n "${target}" ; then
+ builtin compgen -W "${target}" -- "${cmdline[5]}"
+ else
+ packages "${cmdline[4]}" -- "${cmdline[5]}"
+ fi
+ fi
+}
+
+#
+# The main options
+#
+let remove=0
+while test "${cmdline[1+remove]::1}" = "-" ; do
+ case "${cmdline[1+remove]}" in
+ -A|--apiurl)
+ if ((count-remove == 1)); then
+ builtin compgen -W "${oscopts[*]}" -- "${cmdline[1+remove]}"
+ exit
+ elif ((count-remove == 2)); then
+ if test -s ~/.oscrc ; then
+ hints=($(sed -rn '/^(aliases=|\[http)/{s/,/ /g;s/(aliases=|\[|\])//gp}' ~/.oscrc|sort -u))
+ builtin compgen -W "${hints[*]}" -- "${cmdline[2+remove]}"
+ else
+ builtin compgen -P https:// -A hostname
+ fi
+ exit
+ fi
+ let remove+=2
+ ;;
+ -c|--config)
+ if ((count-remove == 1)); then
+ builtin compgen -W "${oscopts[*]}" -- "${cmdline[1+remove]}"
+ exit
+ elif ((count-remove == 2)); then
+ builtin compgen -o filenames -o bashdefault -f -X '.osc' -- "${cmdline[2+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == 1)); then
+ builtin compgen -W "${oscopts[*]}" -- "${cmdline[1+remove]}"
+ exit
+ fi
+ let remove++
+ ;;
+ *) break
+ esac
+done
+if ((remove)) ; then
+ cmdline=(${cmdline[0]} ${cmdline[@]:remove+1})
+ let count-=remove
+ let remove=0
+fi
+
+case "${cmdline[1]}" in
+add|addremove|ar)
+ opts=(--help --recursive)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ else
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count >= 2)) ; then
+ targets ${opts[*]} -- "${cmdline[count]}"
+ fi
+ ;;
+build)
+ opts=(--help --oldpackages --disable-cpio-bulk-download --release --baselibs
+ --disable-debuginfo --debuginfo --alternative-project --vm-type --linksources
+ --local-package --build-uid --userootforbuild --define --without --with
+ --ccache --icecream --jobs --root --extra-pkgs --keep-pkgs --prefer-pkgs
+ --noservice --no-service --no-verify --nochecks --no-checks --noinit --no-init
+ --overlay --rsync-dest --rsync-src --no-changelog --preload --offline --clean)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --alternative-project)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ projects -- "${cmdline[count]}"
+ exit
+ elif ((count-remove == off+2)) ; then
+ repositories "${cmdline[off+1+remove]}" -- "${cmdline[off+2+$remove]}"
+ exit
+ elif ((count-remove == off+3)) ; then
+ architectures "${cmdline[off+1+remove]}" -- "${cmdline[off+3+remove]}"
+ exit
+ fi
+ let remove+=4
+ ;;
+ --define)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ builtin compgen -P "'" -S "'" -W 'macro\ definition' -- "${cmdline[off+1+remvoe]}"
+ exit
+ elif ((count-remove == off+2)) ; then
+ exit
+ fi
+ let remove+=3
+ ;;
+ --@(root|oldpackages|keep-pkgs|prefer-pkgs|rsync-dest|rsync-src))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ builtin compgen -o dirnames -d -- ${cmdline[off+1+remove]}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --@(release|icecream|jobs|without|with|overlay))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --build-uid)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ builtin compgen -W "399:399 $(id -u):$(id -g)" -- "${cmdline[off+2+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ specs=($(command ls *.spec))
+ builtin compgen -W "${opts[*]} ${specs[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+branch|getpac|bco|branchco)
+ opts=(--help --revision --new-package --maintenance --noaccess --extend-package-names
+ --add-repositories --force --nodevelproject)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ case "${cmdline[1]}" in
+ branch) opts[${#opts[@]}]=--checkout
+ esac
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --revision)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 6)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[4]}"
+ projects -- "${cmdline[4]}"
+ elif ((count == 5)) ; then
+ packages "${cmdline[4]}" -- "${cmdline[5]}"
+ elif ((count == 6)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[6]}"
+ fi
+ ;;
+list|ls|ll|LL)
+ opts=(--help --meta --deleted --long --verbose --unexpand --expand --binaries
+ --repo --revision --arch)
+ if ((count == 1)) ; then
+ builtin compgen -W 'list ls ll LL' -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(revision|repo|arch))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ packages -u "${cmdline[2]}" "${cmdline[3]}" -- "${cmdline[4]}"
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+less|cat)
+ opts=(--help --meta --unexpand --expand --revision)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --revision)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ packages -u "${cmdline[2]}" "${cmdline[3]}" -- "${cmdline[4]}"
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ fi
+ ;;
+sr|submitpac|submitreq|submitrequest)
+ opts=(--help --yes --diff --no-update --no-cleanup --cleanup --seperate-requests
+ --nodevelproject --supersede --revision)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(revision|supersede))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 6)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count >= 2 && count <= 5)) ; then
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ fi
+ submit $((count-1)) 1
+ elif ((count == 6)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[6]}"
+ fi
+ ;;
+rq|request|review)
+ opts=(--help --involved-projects --exclude-target-project --non-interactive --interactive --edit)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ while test "${cmdline[2+remove]::1}" = "-" ; do
+ case "${cmdline[2+remove]}" in
+ --exclude-target-project)
+ if ((count-remove == 2)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ elif ((count-remove == 3)) ; then
+ builtin echo -n EXCLUDE_TARGET_PROJECT
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:2} ${cmdline[@]:remove+2})
+ let count-=remove
+ let remove=0
+ fi
+ fi
+ case "${cmdline[2]}" in
+ log|checkout)
+ opts=(--help)
+ if ((count == 2)) ; then
+ builtin compgen -W 'log checkout' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ builtin echo -n 'ID'
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+ revoke|clone)
+ opts=(--help)
+ if ((count == 2)) ; then
+ builtin compgen -W 'revoke clone' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 4)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ builtin echo -n 'ID'
+ elif ((count == 4)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[4]}"
+ fi
+ ;;
+ setincident)
+ opts=(--help)
+ if ((count == 2)) ; then
+ builtin compgen -W 'setincident' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 4)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ builtin echo -n 'ID'
+ elif ((count == 4)) ; then
+ builtin echo -n 'INCIDENT'
+ elif ((count == 5)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[5]}"
+ fi
+ ;;
+ supersede|add|accept|decline|reopen)
+ case "${cmdline[1]}" in
+ rq|request) opts=() ;;
+ review) opts=(--user --group --project --package)
+ esac
+ if ((count == 2)) ; then
+ builtin compgen -W 'supersede add accept decline reopen' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ typeset -i supersede=0
+ case "${cmdline[2]}" in
+ supersede) let supersede=1
+ esac
+ typeset project=""
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --user)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ user=($(users ${cmdline[off+1+remove]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[off+1+remove]}
+ fi
+ let remove+=2
+ ;;
+ --group)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --project)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ projects -- "${cmdline[off+1+remove]}"
+ exit
+ else
+ project="${cmdline[off+1+remove]}"
+ fi
+ let remove+=2
+ ;;
+ --package)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ test -z "$project" && project=PROJECT_REQUIRED
+ packages -u "$project" -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 4)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts+${opts[*]} }ID" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ if ((supersede)) ; then
+ builtin echo -n 'SUPERSEDING_ID'
+ else
+ builtin compgen -W '--message' -- "${cmdline[4]}"
+ fi
+ elif ((count == 5 && supersede)) ; then
+ builtin compgen -W '--message' -- "${cmdline[5]}"
+ fi
+ ;;
+ approvenew)
+ opts=(--help)
+ if ((count == 2)) ; then
+ builtin compgen -W 'approvenew' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 4)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ projects -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[4]}"
+ fi
+ ;;
+ show)
+ opts=(--diff --brief --source-buildstatus)
+ if ((count == 2)) ; then
+ builtin compgen -W 'show' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=("${cmdline[@]:0:off}" "${cmdline[@]:remove+off}")
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]} ID" -- "${cmdline[3]}"
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+ list)
+ case "${cmdline[1]}" in
+ rq|request) opts=(--mine --user --state -days --type --bugowner) ;;
+ review) opts=(--user --group --project --package)
+ esac
+ if ((count == 2)) ; then
+ builtin compgen -W 'list' -- "${cmdline[count]}"
+ elif ((count >= 3)) ; then
+ typeset project=""
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --user)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ user=($(users ${cmdline[off+1+remove]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[off+1+remove]}
+ fi
+ let remove+=2
+ ;;
+ --group)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --project)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ projects -- "${cmdline[off+1+remove]}"
+ exit
+ else
+ project="${cmdline[off+1+remove]}"
+ fi
+ let remove+=2
+ ;;
+ --package)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ test -z "$project" && project=PROJECT_REQUIRED
+ packages -u "$project" -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=("${cmdline[@]:0:off}" "${cmdline[@]:remove+off}")
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ projects -- "${cmdline[3]}"
+ fi
+ if ((count == 4)) ; then
+ packages -u "${cmdline[3]}" -- "${cmdline[4]}"
+ fi
+ ;;
+ *)
+ if ((count == 2)) ; then
+ case "${cmdline[1+remove]}" in
+ rq|request) builtin compgen -W "${opts[*]} ${oscreq[*]}" -- "${cmdline[2]}" ;;
+ review) builtin compgen -W "${opts[*]} ${oscrev[*]}" -- "${cmdline[2]}" ;;
+ esac
+ fi
+ esac
+ ;;
+my)
+ opts=(--help --maintained --verbose --exclude-project --user --all --maintainer --bugowner)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --user)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ user=($(users ${cmdline[off+1+remove]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[off+1+remove]}
+ fi
+ let remove+=2
+ ;;
+ --exclude-project)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ projects -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]} ${oscmy[*]}" -- "${cmdline[2]}"
+ elif ((count >= 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ fi
+ ;;
+copypac|linkpac)
+ opts=(--help --expand --to-apiurl --revision --keep-develproject --keep-link
+ --keep-maintainers --client-side-copy)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(revision|to-apiurl))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ projects -- "${cmdline[4]}"
+ elif ((count == 5)) ; then
+ packages"${cmdline[4]}" -- "${cmdline[5]}"
+ elif ((count == 6)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[6]}"
+ fi
+ ;;
+delete)
+ opts=(--help --force)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ else
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count >= 2)) ; then
+ targets ${opts[*]} -- "${cmdline[count]}"
+ fi
+ ;;
+deleterequest|deletereq|droprequest|dropreq|dr)
+ typeset -i repository=0
+ opts=(--help --repository --message)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count == 2)) ; then
+ projects -- "${cmdline[2]}"
+ elif ((count >= 3)) ; then
+ for ((off=3; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --repository)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ let repository++
+ ;;
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 4)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 3)) ; then
+ if ((repository)) ; then
+ builtin compgen -W '--message' -- "${cmdline[4]}"
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ fi
+ elif ((count == 4)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[4]}"
+ fi
+ ;;
+changedevelrequest|changedevelreq|cr)
+ opts=(--help)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[4]}"
+ elif ((count == 5)) ; then
+ packages "${cmdline[4]}" -- "${cmdline[5]}"
+ fi
+ ;;
+rdiff)
+ opts=(--help --unexpand --missingok --change --plain --revision --meta)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(revision|change))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[4]}"
+ projects -- "${cmdline[4]}"
+ elif ((count == 5)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[5]}"
+ packages "${cmdline[4]}" -- "${cmdline[5]}"
+ fi
+ ;;
+ci|commit|checkin)
+ opts=(--help --skip-local-service-run --noservice --verbose --skip-validation --force
+ --file --message)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --file)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --message) exit ;;
+ -*)
+ if ((count-remove == off)) ; then
+ ((count >= 3)) && opts[${#opts[@]}]=--message
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ targets ${opts[*]} -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ builtin compgen -W "--message ${opts[*]}" -- "${cmdline[3]}"
+ fi
+ ;;
+co|checkout)
+ opts=(--help --limit-size --server-side-source-service-files --source-service-files
+ --output-dir --current-dir --meta --unexpand-link --expand-link --revision)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(output-dir|revision))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ ;;
+ *) break
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count == 4)) ; then
+ packages "${cmdline[2]}" "${cmdline[3]}" -- "${cmdline[4]}"
+ fi
+ ;;
+maintainer)
+ opts=(--help --role --delete --set-bugowner-request --set-bugowner --all --add
+ --devel-project --verbose --nodevelproject --email --bugowner --bugowner-only)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(delete|set-bugowner-request|set-bugowner|add|devel-projec))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --devel-projec)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ projects -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ --role)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ builtin compgen -W 'bugowner maintainer involved' -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count > 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ if ((count == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ fi
+ fi
+ ;;
+up|update)
+ opts=(--help --limit-size --server-side-source-service-files --source-service-files
+ --expand-link --unexpand-link --revision)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ while test "${cmdline[2+remove]::1}" = "-" ; do
+ case "${cmdline[2+remove]}" in
+ --revision)
+ if ((count-remove == 2)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ elif ((count-remove == 3)) && test -z "${cmdline[3+remove]}" ; then
+ hint="${cmdline[2+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ fi
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ ;;
+meta)
+ opts=(--help --delete --set --remove-linking-repositories --create --edit --file
+ --force --attribute-project --attribute-defaults --attribute)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(attribute|file))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --set)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ builtin echo -n ATTRIBUTE_VALUES
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W 'prj pkg prjconf user pattern attribute' -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ if test "${cmdline[2]}" = user ; then
+ user=($(users ${cmdline[3]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[3]}
+ else
+ projects -- "${cmdline[3]}"
+ fi
+ elif ((count == 4)) ; then
+ if test "${cmdline[2]}" = pkg ; then
+ packages "${cmdline[3]}" -- "${cmdline[4]}"
+ elif test "${cmdline[2]}" = attribute ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[4]}"
+ packages "${cmdline[3]}" -- "${cmdline[4]}"
+ elif test "${cmdline[2]}" = user ; then
+ user=($(users ${cmdline[4]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[4]}
+ else
+ builtin compgen -W "${opts[*]}" -- ${cmdline[4]}
+ fi
+ elif ((count == 5)) ; then
+ builtin compgen -W "${opts[*]}" -- ${cmdline[5]}
+ fi
+ ;;
+wipebinaries)
+ opts=(--help --all --unresolvable --broken --build-failed --build-disabled --repo --arch)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(repo|arch))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count-remove == 3)) ; then
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ fi
+ ;;
+help)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ else
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[2]}"
+ fi
+ ;;
+search)
+ opts=(--help --all --binaryversion --baseproject --binary --csv --mine
+ --maintained --maintainer --bugowner --involved --version --verbose
+ --limit-to-attribute --description --title --project --package
+ --substring --exact --repos-baseurl)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ while test "${cmdline[2+remove]::1}" = "-" ; do
+ case "${cmdline[2+remove]}" in
+ --@(binaryversion|baseproject|limit-to-attribute))
+ if ((count-remove == 2)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ elif ((count-remove == 3)) && test -z "${cmdline[3+remove]}" ; then
+ hint="${cmdline[2+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --@(maintainer|bugowner|involved))
+ if ((count-remove == 2)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ elif ((count-remove == 3)) ; then
+ user=($(users ${cmdline[3+remove]}))
+ builtin compgen -W "${user[*]}" -- ${cmdline[3+remove]}
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ fi
+ if ((count-remove == 2)) ; then
+ builtin compgen -W "${opts[*]} SEARCH_TERM" -- "${cmdline[count]}"
+ fi
+ ;;
+pr|prjresults)
+ opts=(--help --show-excluded --vertical --repo --arch --name-filter --status-filter
+ --xml --csv --hide-legend)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[2]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(repo|arch))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --name-filter)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ builtin echo -n EXPR
+ exit
+ fi
+ let remove+=2
+ ;;
+ --status-filter)
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) ; then
+ status=(disabled failed finished building succeeded broken scheduled unresolvable signing blocked)
+ builtin compgen -W "${status[*]}" -- "${cmdline[off+1+remove]}"
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ else
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+r|results)
+ opts=(--help --format --csv --xml --watch --verbose --arch --repo --last-build)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(repo|arch|format))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count > 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+diff|linkdiff)
+ opts=(--help --missingok --link --plain --revision --change)
+ typeset -i link=0
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ case "${cmdline[1]}" in
+ linkdiff) let link++ ;;
+ esac
+ for ((off=2; off<=count; off++)) ; do
+ while test "${cmdline[off+remove]::1}" = "-" ; do
+ case "${cmdline[off+remove]}" in
+ --@(revision|change))
+ if ((count-remove == off)); then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ elif ((count-remove == off+1)) && test -z "${cmdline[off+1+remove]}" ; then
+ hint="${cmdline[off+remove]^^}"
+ builtin echo -n ${hint##*-}
+ exit
+ fi
+ let remove+=2
+ ;;
+ --link)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ let link++
+ ;;
+ -*)
+ if ((count-remove == off)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[off+remove]}"
+ exit
+ fi
+ let remove++
+ esac
+ done
+ if ((remove)) ; then
+ cmdline=(${cmdline[*]:0:off} ${cmdline[@]:remove+off})
+ let count-=remove
+ let remove=0
+ fi
+ done
+ fi
+ if ((count == 2)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[2]}"
+ ((link)) && projects -- "${cmdline[2]}"
+ elif ((count == 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[3]}"
+ ((link)) && packages "${cmdline[2]}" -- "${cmdline[3]}"
+ elif ((count > 3)) ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ fi
+ ;;
+*)
+ opts=(--help)
+ if ((count == 1)) ; then
+ builtin compgen -W "${osccmds[*]} ${oscopts[*]}" -- "${cmdline[count]}"
+ elif ((count >= 2)) ; then
+ if test "${cmdline[count]::1}" = "-" ; then
+ builtin compgen -W "${opts[*]}" -- "${cmdline[count]}"
+ else
+ targets ${opts[*]} -- "${cmdline[count]}"
+ fi
+ fi
+esac
diff --git a/docs/_static/.keepme b/docs/_static/.keepme
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/_static/.keepme
diff --git a/docs/api/modules.rst b/docs/api/modules.rst
new file mode 100644
index 0000000..26c0985
--- /dev/null
+++ b/docs/api/modules.rst
@@ -0,0 +1,10 @@
+osc
+===
+
+These are the packages in the osc package.
+
+.. toctree::
+ :maxdepth: 4
+
+ osc.core
+ osc.util
diff --git a/docs/api/osc.core.rst b/docs/api/osc.core.rst
new file mode 100644
index 0000000..b4840ba
--- /dev/null
+++ b/docs/api/osc.core.rst
@@ -0,0 +1,20 @@
+.. py:module:: osc.core
+
+core
+====
+
+This is the osc core module.
+
+basic structures
+----------------
+
+.. autoclass:: File
+ :members:
+
+
+.. autoclass:: Serviceinfo
+ :members:
+
+
+.. autoclass:: Linkinfo
+ :members:
diff --git a/docs/api/osc.util.rst b/docs/api/osc.util.rst
new file mode 100644
index 0000000..e00bd62
--- /dev/null
+++ b/docs/api/osc.util.rst
@@ -0,0 +1,78 @@
+osc.util package
+================
+
+Submodules
+----------
+
+osc.util.ar module
+------------------
+
+.. automodule:: osc.util.ar
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.archquery module
+-------------------------
+
+.. automodule:: osc.util.archquery
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.cpio module
+--------------------
+
+.. automodule:: osc.util.cpio
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.debquery module
+------------------------
+
+.. automodule:: osc.util.debquery
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.packagequery module
+----------------------------
+
+.. automodule:: osc.util.packagequery
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.repodata module
+------------------------
+
+.. automodule:: osc.util.repodata
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.rpmquery module
+------------------------
+
+.. automodule:: osc.util.rpmquery
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+osc.util.safewriter module
+--------------------------
+
+.. automodule:: osc.util.safewriter
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: osc.util
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/api/tutorial.rst b/docs/api/tutorial.rst
new file mode 100644
index 0000000..01a440d
--- /dev/null
+++ b/docs/api/tutorial.rst
@@ -0,0 +1,96 @@
+Tutorial
+========
+
+This is a tutorial on how to use the osc python api.
+
+Key to the |obs| are (remote):
+
+ #. A **project**
+ #. A project has associated multiple **repositories** (linux distributions)
+ #. Multiple **packages** in a project will hold the builds against the difefrent **repositories**
+
+
+A user will deal with local checkout of a project in a **working copy**: this is similar to the
+subversion checkout model.
+
+
+Initial config setup
+--------------------
+
+Osc the library requires an initial setup:
+
+ >>> import osc.conf
+ >>> osc.conf.get_config()
+
+This will read all the external config files (eg. ~/.oscrc) and the internal configuration
+values.
+
+
+Acquiring the apiurl
+--------------------
+
+All the osc operation will use a **apiurl** to lookup for things like passwords, username and other parameters
+while performing operations:
+
+ >>> apiurl = osc.conf.config['apiurl']
+
+
+Operations on a remote build server
+-----------------------------------
+
+osc is similar to subversion, it has a remote server and a local (checkout) **working** directory.
+First we'll go through the remote operation on a server **NOT** requiring a checkout.
+Operations are contained in the osc.core module:
+
+ >>> import osc.core
+
+
+List all the projects and packages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This will show all the projects on the remote |obs|:
+
+ >>> for prj in osc.core.meta_get_project_list(apiurl, deleted=False):
+ print prj
+
+
+A project has **repositories** associated with it (eg. linux distributions):
+
+ >>> prj = 'home:cavallo71:opt-python-interpreters'
+ >>> for repo in osc.core.get_repos_of_project(apiurl, prj):
+ print repo
+
+
+A project contains packages and to list them all:
+
+ >>> prj = 'home:cavallo71:opt-python-interpreters'
+ >>> for pkg in osc.core.meta_get_packagelist(apiurl, prj):
+ print pkg
+
+
+Add a package to an existing project
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+Operations in a checked out **working copy**
+--------------------------------------------
+
+
+
+Create your first project: the hello project
+--------------------------------------------
+
+.. todo:: add he description on how to init a project
+
+
+Adding your firs package to the project hello: the world package
+----------------------------------------------------------------
+
+.. todo:: add he description on how to add a package
+
+
+
+Setting the build architectures
+-------------------------------
+
+
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..a3c582a
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,298 @@
+# -*- coding: utf-8 -*-
+#
+# osc documentation build configuration file, created by
+# sphinx-quickstart on Sun Jan 24 13:06:29 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+import shlex
+
+
+# top level dir (one above this file)
+topdir = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
+
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, topdir)
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.todo',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.ifconfig',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'osc'
+copyright = u'2016, see authors list'
+author = u'see authors list'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.2.3'
+# The full version, including alpha/beta/rc tags.
+release = '4.5.6'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'oscdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'osc.tex', u'osc Documentation',
+ u'see authors list', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'osc', u'osc Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'osc', u'osc Documentation',
+ author, 'osc', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+rst_epilog = """
+.. |obs| replace:: open build service
+"""
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..2fcde0a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,33 @@
+.. osc documentation master file, created by
+ sphinx-quickstart on Sun Jan 24 13:06:29 2016.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to osc's documentation!
+===============================
+
+This is the documentation for the osc python client to the |obs|.
+
+Tutorial
+
+.. TODO:: add more documentation
+
+
+
+API:
+
+.. toctree::
+ :maxdepth: 2
+
+ api/tutorial
+ api/modules
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/fuse/fuseosc b/fuse/fuseosc
new file mode 100755
index 0000000..08489ea
--- /dev/null
+++ b/fuse/fuseosc
@@ -0,0 +1,234 @@
+#!/usr/bin/python
+
+# Copyright (c) 2008-2009 Pavol Rusnak <prusnak@suse.cz>
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+
+import sys
+import os
+try:
+ import osc
+ import osc.conf
+ import osc.core
+except:
+ # allow loading module from working copy if osc is not installed
+ sys.path.append(os.path.abspath(os.path.dirname(sys.argv[0]) + '/../osc'))
+ import osc
+ import osc.conf
+ import osc.core
+import fuse
+import stat
+import errno
+import tempfile
+
+fuse.fuse_python_api = (0, 2)
+
+projects = []
+cache = {}
+
+class EmptyStat(fuse.Stat):
+ def __init__(self):
+ self.st_mode = 0
+ self.st_ino = 0
+ self.st_dev = 0
+ self.st_nlink = 0
+ self.st_uid = 0
+ self.st_gid = 0
+ self.st_size = 0
+ self.st_atime = 0
+ self.st_mtime = 0
+ self.st_ctime = 0
+
+class CacheEntry(object):
+ def __init__(self):
+ self.stat = None
+ self.handle = None
+ self.tmpname = None
+
+class oscFS(fuse.Fuse):
+
+ def __init__(self, *args, **kw):
+ fuse.Fuse.__init__(self, *args, **kw)
+ print 'OK'
+
+ def getattr(self, path):
+ st = EmptyStat()
+ # path is project
+ if path == '/' or path in projects or len(filter(lambda x: x.startswith(path), projects)) > 0:
+ st.st_mode = stat.S_IFDIR | 0555
+ st.st_nlink = 2
+ return st
+ # path is package
+ if os.path.dirname(path) in projects:
+ st.st_mode = stat.S_IFDIR | 0555
+ st.st_nlink = 2
+ return st
+ # path is file
+ if cache.has_key(path):
+ return cache[path].stat
+ else:
+ return -errno.ENOENT
+
+ def readdir(self, path, offset):
+ yield fuse.Direntry('.')
+ yield fuse.Direntry('..')
+
+ if os.path.dirname(path) in projects: # path is package
+ prj = os.path.dirname(path).replace('/','')
+ pkg = os.path.basename(path)
+ for f in osc.core.meta_get_filelist(osc.conf.config['apiurl'], prj, pkg, verbose=True):
+ st = EmptyStat()
+ st.st_mode = stat.S_IFREG | 0444
+ st.st_size = f.size
+ st.st_atime = f.mtime
+ st.st_ctime = f.mtime
+ st.st_mtime = f.mtime
+ cache[path + '/' + f.name] = CacheEntry()
+ cache[path + '/' + f.name].stat = st
+ yield fuse.Direntry(f.name)
+ return
+
+ if path in projects: # path is project
+ prj = path.replace('/','')
+ for p in osc.core.meta_get_packagelist(osc.conf.config['apiurl'], prj):
+ yield fuse.Direntry(p)
+
+ else: # path is project structure
+ if (path != '/'):
+ path += '/'
+ l = len(path)
+ for d in set( map(lambda x: x[l:].split('/')[0], filter(lambda x: x.startswith(path), projects) ) ) :
+ yield fuse.Direntry(d)
+
+ def mythread ( self ):
+ print '*** mythread'
+ return -errno.ENOSYS
+
+ def chmod ( self, path, mode ):
+ print '*** chmod', path, oct(mode)
+ return -errno.ENOSYS
+
+ def chown ( self, path, uid, gid ):
+ print '*** chown', path, uid, gid
+ return -errno.ENOSYS
+
+ def fsync ( self, path, isFsyncFile ):
+ print '*** fsync', path, isFsyncFile
+ return -errno.ENOSYS
+
+ def link ( self, targetPath, linkPath ):
+ print '*** link', targetPath, linkPath
+ return -errno.ENOSYS
+
+ def mkdir ( self, path, mode ):
+ print '*** mkdir', path, oct(mode)
+ return -errno.ENOSYS
+
+ def mknod ( self, path, mode, dev ):
+ print '*** mknod', path, oct(mode), dev
+ return -errno.ENOSYS
+
+ def open ( self, path, flags ):
+ file = os.path.basename(path)
+ d = os.path.dirname(path)
+ pkg = os.path.basename(d)
+ prj = os.path.dirname(d).replace('/','')
+ if not cache.has_key(path):
+ return -errno.ENOENT
+ if cache[path].stat == None:
+ return -errno.ENOENT
+ tmp = tempfile.mktemp(prefix = 'oscfs_')
+ osc.core.get_source_file(osc.conf.config['apiurl'], prj, pkg, file, tmp)
+ cache[path].handle = open(tmp, 'r')
+ cache[path].tmpname = tmp
+
+ def read ( self, path, length, offset ):
+ if not cache.has_key(path):
+ return -errno.EACCES
+ f = cache[path].handle
+ f.seek(offset)
+ return f.read(length)
+
+ def readlink ( self, path ):
+ print '*** readlink', path
+ return -errno.ENOSYS
+
+ def release ( self, path, flags ):
+ if cache.has_key(path):
+ cache[path].handle.close()
+ cache[path].handle = None
+ os.unlink(f.cache[path].tmpname)
+ cache[path].tmpname = None
+
+ def rename ( self, oldPath, newPath ):
+ print '*** rename', oldPath, newPath
+ return -errno.ENOSYS
+
+ def rmdir ( self, path ):
+ print '*** rmdir', path
+ return -errno.ENOSYS
+
+ def statfs ( self ):
+ print '*** statfs'
+ return -errno.ENOSYS
+
+ def symlink ( self, targetPath, linkPath ):
+ print '*** symlink', targetPath, linkPath
+ return -errno.ENOSYS
+
+ def truncate ( self, path, size ):
+ print '*** truncate', path, size
+ return -errno.ENOSYS
+
+ def unlink ( self, path ):
+ print '*** unlink', path
+ return -errno.ENOSYS
+
+ def utime ( self, path, times ):
+ print '*** utime', path, times
+ return -errno.ENOSYS
+
+ def write ( self, path, buf, offset ):
+ print '*** write', path, buf, offset
+ return -errno.ENOSYS
+
+def fill_projects():
+ try:
+ for prj in osc.core.meta_get_project_list(osc.conf.config['apiurl']):
+ projects.append( '/' + prj.replace(':', ':/') )
+ except:
+ print 'failed'
+ sys.exit(1)
+
+if __name__ == '__main__':
+ print 'Loading config ...',
+ osc.conf.get_config()
+ print 'OK'
+ print 'Getting projects list ...',
+ fill_projects()
+ print 'OK'
+ print 'Starting FUSE ...',
+ oscfs = oscFS( version = '%prog ' + fuse.__version__, usage = '', dash_s_do = 'setsingle')
+ oscfs.flags = 0
+ oscfs.multithreaded = 0
+ oscfs.parse(values = oscfs, errex = 1)
+ oscfs.main()
diff --git a/fuse/start b/fuse/start
new file mode 100755
index 0000000..ebdde5e
--- /dev/null
+++ b/fuse/start
@@ -0,0 +1,3 @@
+#!/bin/sh
+mkdir -p ./test
+./fuseosc ./test
diff --git a/fuse/stop b/fuse/stop
new file mode 100755
index 0000000..a1c55a7
--- /dev/null
+++ b/fuse/stop
@@ -0,0 +1,2 @@
+#!/bin/sh
+fusermount -u ./test
diff --git a/osc-wrapper.py b/osc-wrapper.py
new file mode 100755
index 0000000..833729e
--- /dev/null
+++ b/osc-wrapper.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+# this wrapper exists so it can be put into /usr/bin, but still allows the
+# python module to be called within the source directory during development
+
+import locale
+import sys
+import os
+
+from osc import commandline, babysitter
+
+try:
+# this is a hack to make osc work as expected with utf-8 characters,
+# no matter how site.py is set...
+ reload(sys)
+ loc = locale.getpreferredencoding()
+ if not loc:
+ loc = sys.getpreferredencoding()
+ sys.setdefaultencoding(loc)
+ del sys.setdefaultencoding
+except NameError:
+ #reload, neither setdefaultencoding are in python3
+ pass
+
+# avoid buffering output on pipes (bnc#930137)
+# Basically, a "print('foo')" call is translated to a corresponding
+# fwrite call that writes to the stdout stream (cf. string_print
+# (Objects/stringobject.c) and builtin_print (Python/bltinmodule.c));
+# If no pipe is used, stdout is a tty/refers to a terminal =>
+# the stream is line buffered (see _IO_file_doallocate (libio/filedoalloc.c)).
+# If a pipe is used, stdout does not refer to a terminal anymore =>
+# the stream is fully buffered by default (see _IO_file_doallocate).
+# The following fdopen call makes stdout line buffered again (at least on
+# systems that support setvbuf - if setvbuf is not supported, the stream
+# remains fully buffered (see PyFile_SetBufSize (Objects/fileobject.c))).
+if not os.isatty(sys.stdout.fileno()):
+ sys.stdout = os.fdopen(sys.stdout.fileno(), sys.stdout.mode, 1)
+
+osccli = commandline.Osc()
+
+r = babysitter.run(osccli)
+sys.exit(r)
diff --git a/osc.fish b/osc.fish
new file mode 100644
index 0000000..09dac9d
--- /dev/null
+++ b/osc.fish
@@ -0,0 +1,116 @@
+# fish completion for git
+# vim: smartindent:expandtab:ts=2:sw=2
+
+function __fish_osc_needs_command
+ set cmd (commandline -opc)
+ if contains "$cmd" 'osc' 'osc help'
+ return 0
+ end
+ return 1
+end
+
+function __fish_osc_using_command
+ set cmd (commandline -opc)
+ if [ (count $cmd) -gt 1 ]
+ for arg in $argv
+ if [ $arg = $cmd[2] ]
+ return 0
+ end
+ end
+ end
+ return 1
+end
+
+# general options
+complete -f -c osc -n 'not __fish_osc_needs_command' -s A -l apiurl -d 'specify URL to access API server at or an alias'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s c -l config -d 'specify alternate configuration file'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s d -l debug -d 'print info useful for debugging'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l debugger -d 'jump into the debugger before executing anything'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s h -l help -d 'show this help message and exit'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s H -l http-debug -d 'debug HTTP traffic (filters some headers)'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l http-full-debug -d 'debug HTTP traffic (filters no headers)'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l no-gnome-keyring -d 'disable usage of GNOME Keyring'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l no-keyring -d 'disable usage of desktop keyring system'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l post-mortem -d 'jump into the debugger in case of errors'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s q -l quiet -d 'be quiet, not verbose'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s t -l traceback -d 'print call trace in case of errors'
+complete -f -c osc -n 'not __fish_osc_needs_command' -s v -l verbose -d 'increase verbosity'
+complete -f -c osc -n 'not __fish_osc_needs_command' -l version -d 'show program\'s version number and exit'
+
+# osc commands
+complete -f -c osc -n '__fish_osc_needs_command' -a 'add' -d 'Mark files to be added upon the next commit'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'addremove ar' -d 'Adds new files, removes disappeared files'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'aggregatepac' -d '"Aggregate" a package to another package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'api' -d 'Issue an arbitrary request to the API'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'branch bco branchco getpac' -d 'Branch a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'chroot' -d 'into the buildchroot'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'clean' -d 'removes all untracked files from the package working ...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'commit checkin ci' -d 'Upload content to the repository server'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'config' -d 'get/set a config option'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'copypac' -d 'Copy a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'createincident' -d 'Create a maintenance incident'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'createrequest creq' -d 'create multiple requests with a single command'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'delete del remove rm' -d 'Mark files or package directories to be deleted upon ...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'deleterequest deletereq dr dropreq droprequest' -d 'Request to delete (or "drop") a package or project'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'dependson whatdependson' -d 'Show the build dependencies'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'detachbranch' -d 'replace a link with its expanded sources'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'develproject bsdevelproject dp' -d 'print the devel project / package of a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'diff di ldiff linkdiff' -d 'Generates a diff'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'distributions dists' -d 'Shows all available distributions'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'getbinaries' -d 'Download binaries to a local directory'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'help ? h' -d 'give detailed help on a specific sub-command'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'importsrcpkg' -d 'Import a new package from a src.rpm'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'info' -d 'Print information about a working copy'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'init' -d 'Initialize a directory as working copy'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'jobhistory jobhist' -d 'Shows the job history of a project'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'linkpac' -d '"Link" a package to another package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'linktobranch' -d 'Convert a package containing a classic link with patc...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'list LL lL ll ls' -d 'List sources or binaries on the server'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'localbuildlog lbl' -d 'Shows the build log of a local buildchroot'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'log' -d 'Shows the commit log of a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'maintainer bugowner' -d 'Show maintainers according to server side configuration'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'maintenancerequest mr' -d 'Create a request for starting a maintenance incident.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'man' -d 'generates a man page'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'mbranch maintained sm' -d 'Search or banch multiple instances of a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'meta' -d 'Show meta information, or edit it'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'mkpac' -d 'Create a new package under version control'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'mv' -d 'Move SOURCE file to DEST and keep it under version co...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'my' -d 'show waiting work, packages, projects or requests inv...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'patchinfo' -d 'Generate and edit a patchinfo file.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'pdiff' -d 'Quick alias to diff the content of a package with its...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'prdiff projdiff projectdiff' -d 'Server-side diff of two projects'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'prjresults pr' -d 'Shows project-wide build results'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'pull' -d 'merge the changes of the link target into your workin...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'rdelete' -d 'Delete a project or packages on the server.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'rdiff' -d 'Server-side "pretty" diff of two packages'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'rebuild rebuildpac' -d 'Trigger package rebuilds'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'release' -d 'Release sources and binaries'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'releaserequest' -d 'Create a request for releasing a maintenance update.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'remotebuildlog rbl rblt rbuildlog rbuildlogtail remotebuildlogtail' -d 'Shows the build log of a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'repairlink' -d 'Repair a broken source link'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'repairwc' -d 'try to repair an inconsistent working copy'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'repositories platforms repos' -d 'shows repositories configured for a project. It skips...'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'repourls' -d 'Shows URLs of .repo files'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'request review rq' -d 'Show or modify requests and reviews'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'requestmaintainership reqbs reqbugownership reqmaintainership reqms requestbugownership' -d 'requests to add user as maintainer or bugowner'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'resolved' -d 'Remove "conflicted" state on working copy files'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'restartbuild abortbuild' -d 'Restart the build of a certain project or package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'results r' -d 'Shows the build results of a package or project'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'revert' -d 'Restore changed files or the entire working copy.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'rremove' -d 'Remove source files from selected package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'search bse se' -d 'Search for a project and/or package.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'service' -d 'Handle source services'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'setdevelproject sdp' -d 'Set the devel project / package of a package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'setlinkrev' -d 'Updates a revision number in a source link.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'signkey' -d 'Manage Project Signing Key'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'status st' -d 'Show status of files in working copy'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'submitrequest sr submitpac submitreq' -d 'Create request to submit source into another Project'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'token' -d 'Show and manage authentication token'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'triggerreason tr' -d 'Show reason why a package got triggered to build'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'undelete' -d 'Restores a deleted project or package on the server.'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'unlock' -d 'Unlocks a project or package'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'update up' -d 'Update a working copy'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'updatepacmetafromspec metafromspec updatepkgmetafromspec' -d 'Update package meta information from a specfile'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'vc' -d 'Edit the changes file'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'whois user who' -d 'Show fullname and email of a buildservice user'
+complete -f -c osc -n '__fish_osc_needs_command' -a 'wipebinaries' -d 'Delete all binary packages of a certain project/package'
diff --git a/osc.ico b/osc.ico
new file mode 100644
index 0000000..992764f
--- /dev/null
+++ b/osc.ico
Binary files differ
diff --git a/osc.png b/osc.png
new file mode 100644
index 0000000..097c6f2
--- /dev/null
+++ b/osc.png
Binary files differ
diff --git a/osc/OscConfigParser.py b/osc/OscConfigParser.py
new file mode 100644
index 0000000..bf154b1
--- /dev/null
+++ b/osc/OscConfigParser.py
@@ -0,0 +1,360 @@
+# Copyright 2008,2009 Marcus Huewe <suse-tux@gmx.de>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 2
+# as published by the Free Software Foundation;
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import print_function
+
+import sys
+
+if sys.version_info >= ( 3, ):
+ import configparser
+else:
+ #python 2.x
+ import ConfigParser as configparser
+
+import re
+
+# inspired from http://code.google.com/p/iniparse/ - although their implementation is
+# quite different
+
+class ConfigLineOrder:
+ """
+ A ConfigLineOrder() instance task is to preserve the order of a config file.
+ It keeps track of all lines (including comments) in the _lines list. This list
+ either contains SectionLine() instances or CommentLine() instances.
+ """
+ def __init__(self):
+ self._lines = []
+
+ def _append(self, line_obj):
+ self._lines.append(line_obj)
+
+ def _find_section(self, section):
+ for line in self._lines:
+ if line.type == 'section' and line.name == section:
+ return line
+ return None
+
+ def add_section(self, sectname):
+ self._append(SectionLine(sectname))
+
+ def get_section(self, sectname):
+ section = self._find_section(sectname)
+ if section:
+ return section
+ section = SectionLine(sectname)
+ self._append(section)
+ return section
+
+ def add_other(self, sectname, line):
+ if sectname:
+ self.get_section(sectname).add_other(line)
+ else:
+ self._append(CommentLine(line))
+
+ def keys(self):
+ return [ i.name for i in self._lines if i.type == 'section' ]
+
+ def __setitem__(self, key, value):
+ section = SectionLine(key)
+ self._append(section)
+
+ def __getitem__(self, key):
+ section = self._find_section(key)
+ if not section:
+ raise KeyError()
+ return section
+
+ def __delitem__(self, key):
+ line = self._find_section(key)
+ if not line:
+ raise KeyError(key)
+ self._lines.remove(line)
+
+ def __iter__(self):
+ #return self._lines.__iter__()
+ for line in self._lines:
+ if line.type == 'section':
+ yield line.name
+ raise StopIteration()
+
+class Line:
+ """Base class for all line objects"""
+ def __init__(self, name, type):
+ self.name = name
+ self.type = type
+
+class SectionLine(Line):
+ """
+ This class represents a [section]. It stores all lines which belongs to
+ this certain section in the _lines list. The _lines list either contains
+ CommentLine() or OptionLine() instances.
+ """
+ def __init__(self, sectname, dict = {}):
+ Line.__init__(self, sectname, 'section')
+ self._lines = []
+ self._dict = dict
+
+ def _find(self, name):
+ for line in self._lines:
+ if line.name == name:
+ return line
+ return None
+
+ def _add_option(self, optname, value = None, line = None, sep = '='):
+ if value is None and line is None:
+ raise configparser.Error('Either value or line must be passed in')
+ elif value and line:
+ raise configparser.Error('value and line are mutually exclusive')
+
+ if value is not None:
+ line = '%s%s%s' % (optname, sep, value)
+ opt = self._find(optname)
+ if opt:
+ opt.format(line)
+ else:
+ self._lines.append(OptionLine(optname, line))
+
+ def add_other(self, line):
+ self._lines.append(CommentLine(line))
+
+ def copy(self):
+ return dict(self.items())
+
+ def items(self):
+ return [ (i.name, i.value) for i in self._lines if i.type == 'option' ]
+
+ def keys(self):
+ return [ i.name for i in self._lines ]
+
+ def __setitem__(self, key, val):
+ self._add_option(key, val)
+
+ def __getitem__(self, key):
+ line = self._find(key)
+ if not line:
+ raise KeyError(key)
+ return str(line)
+
+ def __delitem__(self, key):
+ line = self._find(key)
+ if not line:
+ raise KeyError(key)
+ self._lines.remove(line)
+
+ def __str__(self):
+ return self.name
+
+ # XXX: needed to support 'x' in cp._sections['sectname']
+ def __iter__(self):
+ for line in self._lines:
+ yield line.name
+ raise StopIteration()
+
+
+class CommentLine(Line):
+ """Store a commentline"""
+ def __init__(self, line):
+ Line.__init__(self, line.strip('\n'), 'comment')
+
+ def __str__(self):
+ return self.name
+
+class OptionLine(Line):
+ """
+ This class represents an option. The class' "name" attribute is used
+ to store the option's name and the "value" attribute contains the option's
+ value. The "frmt" attribute preserves the format which was used in the configuration
+ file.
+ Example:
+ optionx:<SPACE><SPACE>value
+ => self.frmt = '%s:<SPACE><SPACE>%s'
+ optiony<SPACE>=<SPACE>value<SPACE>;<SPACE>some_comment
+ => self.frmt = '%s<SPACE>=<SPACE><SPACE>%s<SPACE>;<SPACE>some_comment
+ """
+
+ def __init__(self, optname, line):
+ Line.__init__(self, optname, 'option')
+ self.name = optname
+ self.format(line)
+
+ def format(self, line):
+ mo = configparser.ConfigParser.OPTCRE.match(line.strip())
+ key, val = mo.group('option', 'value')
+ self.frmt = line.replace(key.strip(), '%s', 1)
+ pos = val.find(' ;')
+ if pos >= 0:
+ val = val[:pos]
+ self.value = val
+ self.frmt = self.frmt.replace(val.strip(), '%s', 1).rstrip('\n')
+
+ def __str__(self):
+ return self.value
+
+
+class OscConfigParser(configparser.SafeConfigParser):
+ """
+ OscConfigParser() behaves like a normal ConfigParser() object. The
+ only differences is that it preserves the order+format of configuration entries
+ and that it stores comments.
+ In order to keep the order and the format it makes use of the ConfigLineOrder()
+ class.
+ """
+ def __init__(self, defaults={}):
+ configparser.SafeConfigParser.__init__(self, defaults)
+ self._sections = ConfigLineOrder()
+
+ # XXX: unfortunately we have to override the _read() method from the ConfigParser()
+ # class because a) we need to store comments b) the original version doesn't use
+ # the its set methods to add and set sections, options etc. instead they use a
+ # dictionary (this makes it hard for subclasses to use their own objects, IMHO
+ # a bug) and c) in case of an option we need the complete line to store the format.
+ # This all sounds complicated but it isn't - we only needed some slight changes
+ def _read(self, fp, fpname):
+ """Parse a sectioned setup file.
+
+ The sections in setup file contains a title line at the top,
+ indicated by a name in square brackets (`[]'), plus key/value
+ options lines, indicated by `name: value' format lines.
+ Continuations are represented by an embedded newline then
+ leading whitespace. Blank lines, lines beginning with a '#',
+ and just about everything else are ignored.
+ """
+ cursect = None # None, or a dictionary
+ optname = None
+ lineno = 0
+ e = None # None, or an exception
+ while True:
+ line = fp.readline()
+ if not line:
+ break
+ lineno = lineno + 1
+ # comment or blank line?
+ if line.strip() == '' or line[0] in '#;':
+ self._sections.add_other(cursect, line)
+ continue
+ if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
+ # no leading whitespace
+ continue
+ # continuation line?
+ if line[0].isspace() and cursect is not None and optname:
+ value = line.strip()
+ if value:
+ #cursect[optname] = "%s\n%s" % (cursect[optname], value)
+ #self.set(cursect, optname, "%s\n%s" % (self.get(cursect, optname), value))
+ if cursect == configparser.DEFAULTSECT:
+ self._defaults[optname] = "%s\n%s" % (self._defaults[optname], value)
+ else:
+ # use the raw value here (original version uses raw=False)
+ self._sections[cursect]._find(optname).value = '%s\n%s' % (self.get(cursect, optname, raw=True), 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 sectname in self._sections:
+ cursect = self._sections[sectname]
+ elif sectname == configparser.DEFAULTSECT:
+ cursect = self._defaults
+ else:
+ #cursect = {'__name__': sectname}
+ #self._sections[sectname] = cursect
+ self.add_section(sectname)
+ self.set(sectname, '__name__', sectname)
+ # So sections can't start with a continuation line
+ cursect = sectname
+ optname = None
+ # no section header in the file?
+ elif cursect is None:
+ raise configparser.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 != -1 and optval[pos-1].isspace():
+ optval = optval[:pos]
+ optval = optval.strip()
+ # allow empty values
+ if optval == '""':
+ optval = ''
+ optname = self.optionxform(optname.rstrip())
+ if cursect == configparser.DEFAULTSECT:
+ self._defaults[optname] = optval
+ else:
+ self._sections[cursect]._add_option(optname, line=line)
+ 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 = configparser.ParsingError(fpname)
+ e.append(lineno, repr(line))
+ # if any parsing errors occurred, raise an exception
+ if e:
+ raise e # pylint: disable-msg=E0702
+
+ def write(self, fp, comments = False):
+ """
+ write the configuration file. If comments is True all comments etc.
+ will be written to fp otherwise the ConfigParsers' default write method
+ will be called.
+ """
+ if comments:
+ fp.write(str(self))
+ fp.write('\n')
+ else:
+ configparser.SafeConfigParser.write(self, fp)
+
+ def has_option(self, section, option, proper=False, **kwargs):
+ """
+ Returns True, if the passed section contains the specified option.
+ If proper is True, True is only returned if the option is owned by
+ this section and not "inherited" from the default.
+ """
+ if proper:
+ return self.optionxform(option) in self._sections[section].keys()
+ return configparser.SafeConfigParser.has_option(self, section, option, **kwargs)
+
+ # XXX: simplify!
+ def __str__(self):
+ ret = []
+ first = True
+ for line in self._sections._lines:
+ if line.type == 'section':
+ if first:
+ first = False
+ else:
+ ret.append('')
+ ret.append('[%s]' % line.name)
+ for sline in line._lines:
+ if sline.name == '__name__':
+ continue
+ if sline.type == 'option':
+ # special handling for continuation lines
+ val = '\n '.join(sline.value.split('\n'))
+ ret.append(sline.frmt % (sline.name, val))
+ elif str(sline) != '':
+ ret.append(str(sline))
+ else:
+ ret.append(str(line))
+ return '\n'.join(ret)
+
+# vim: sw=4 et
diff --git a/osc/__init__.py b/osc/__init__.py
new file mode 100644
index 0000000..7872a4e
--- /dev/null
+++ b/osc/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ['babysitter', 'core', 'commandline', 'oscerr', 'othermethods', 'build', 'fetch', 'meter']
+
+# vim: sw=4 et
diff --git a/osc/babysitter.py b/osc/babysitter.py
new file mode 100644
index 0000000..682de94
--- /dev/null
+++ b/osc/babysitter.py
@@ -0,0 +1,212 @@
+# Copyright (C) 2008 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+from __future__ import print_function
+
+import errno
+import os.path
+import pdb
+import sys
+import signal
+import traceback
+from urlgrabber.grabber import URLGrabError
+
+from osc import oscerr
+from .oscsslexcp import NoSecureSSLError
+from osc.util.cpio import CpioError
+from osc.util.packagequery import PackageError
+
+try:
+ from M2Crypto.SSL.Checker import SSLVerificationError
+ from M2Crypto.SSL import SSLError as SSLError
+except:
+ SSLError = None
+ SSLVerificationError = None
+
+try:
+ # import as RPMError because the class "error" is too generic
+ from rpm import error as RPMError
+except:
+ # if rpm-python isn't installed (we might be on a debian system):
+ RPMError = None
+
+try:
+ from http.client import HTTPException, BadStatusLine
+ from urllib.error import URLError, HTTPError
+except ImportError:
+ #python 2.x
+ from httplib import HTTPException, BadStatusLine
+ from urllib2 import URLError, HTTPError
+
+# the good things are stolen from Matt Mackall's mercurial
+
+
+def catchterm(*args):
+ raise oscerr.SignalInterrupt
+
+for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
+ num = getattr(signal, name, None)
+ if num:
+ signal.signal(num, catchterm)
+
+
+def run(prg, argv=None):
+ try:
+ try:
+ if '--debugger' in sys.argv:
+ pdb.set_trace()
+ # here we actually run the program:
+ return prg.main(argv)
+ except:
+ # look for an option in the prg.options object and in the config
+ # dict print stack trace, if desired
+ if getattr(prg.options, 'traceback', None) or getattr(prg.conf, 'config', {}).get('traceback', None) or \
+ getattr(prg.options, 'post_mortem', None) or getattr(prg.conf, 'config', {}).get('post_mortem', None):
+ traceback.print_exc(file=sys.stderr)
+ # we could use http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215
+ # enter the debugger, if desired
+ if getattr(prg.options, 'post_mortem', None) or getattr(prg.conf, 'config', {}).get('post_mortem', None):
+ if sys.stdout.isatty() and not hasattr(sys, 'ps1'):
+ pdb.post_mortem(sys.exc_info()[2])
+ else:
+ print('sys.stdout is not a tty. Not jumping into pdb.', file=sys.stderr)
+ raise
+ except oscerr.SignalInterrupt:
+ print('killed!', file=sys.stderr)
+ return 1
+ except KeyboardInterrupt:
+ print('interrupted!', file=sys.stderr)
+ return 1
+ except oscerr.UserAbort:
+ print('aborted.', file=sys.stderr)
+ return 1
+ except oscerr.APIError as e:
+ print('BuildService API error:', e.msg, file=sys.stderr)
+ return 1
+ except oscerr.LinkExpandError as e:
+ print('Link "%s/%s" cannot be expanded:\n' % (e.prj, e.pac), e.msg, file=sys.stderr)
+ print('Use "osc repairlink" to fix merge conflicts.\n', file=sys.stderr)
+ return 1
+ except oscerr.WorkingCopyWrongVersion as e:
+ print(e, file=sys.stderr)
+ return 1
+ except oscerr.NoWorkingCopy as e:
+ print(e, file=sys.stderr)
+ if os.path.isdir('.git'):
+ print("Current directory looks like git.", file=sys.stderr)
+ if os.path.isdir('.hg'):
+ print("Current directory looks like mercurial.", file=sys.stderr)
+ if os.path.isdir('.svn'):
+ print("Current directory looks like svn.", file=sys.stderr)
+ if os.path.isdir('CVS'):
+ print("Current directory looks like cvs.", file=sys.stderr)
+ return 1
+ except HTTPError as e:
+ print('Server returned an error:', e, file=sys.stderr)
+ if hasattr(e, 'osc_msg'):
+ print(e.osc_msg, file=sys.stderr)
+
+ try:
+ body = e.read()
+ except AttributeError:
+ body = ''
+
+ if getattr(prg.options, 'debug', None) or \
+ getattr(prg.conf, 'config', {}).get('debug', None):
+ print(e.hdrs, file=sys.stderr)
+ print(body, file=sys.stderr)
+
+ if e.code in [400, 403, 404, 500]:
+ if '<summary>' in body:
+ msg = body.split('<summary>')[1]
+ msg = msg.split('</summary>')[0]
+ print(msg, file=sys.stderr)
+ if e.code >= 500 and e.code <= 599:
+ print('\nRequest: %s' % e.filename)
+ print('Headers:')
+ for h, v in e.hdrs.items():
+ if h != 'Set-Cookie':
+ print("%s: %s" % (h, v))
+
+ return 1
+ except BadStatusLine as e:
+ print('Server returned an invalid response:', e, file=sys.stderr)
+ print(e.line, file=sys.stderr)
+ return 1
+ except HTTPException as e:
+ print(e, file=sys.stderr)
+ return 1
+ except URLError as e:
+ print('Failed to reach a server:\n', e.reason, file=sys.stderr)
+ return 1
+ except URLGrabError as e:
+ print('Failed to grab %s: %s' % (e.url, e.exception), file=sys.stderr)
+ return 1
+ except IOError as e:
+ # ignore broken pipe
+ if e.errno != errno.EPIPE:
+ raise
+ return 1
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ print(e, file=sys.stderr)
+ return 1
+ except (oscerr.ConfigError, oscerr.NoConfigfile) as e:
+ print(e.msg, file=sys.stderr)
+ return 1
+ except oscerr.OscIOError as e:
+ print(e.msg, file=sys.stderr)
+ if getattr(prg.options, 'debug', None) or \
+ getattr(prg.conf, 'config', {}).get('debug', None):
+ print(e.e, file=sys.stderr)
+ return 1
+ except (oscerr.WrongOptions, oscerr.WrongArgs) as e:
+ print(e, file=sys.stderr)
+ return 2
+ except oscerr.ExtRuntimeError as e:
+ print(e.file + ':', e.msg, file=sys.stderr)
+ return 1
+ except oscerr.ServiceRuntimeError as e:
+ print(e.msg, file=sys.stderr)
+ except oscerr.WorkingCopyOutdated as e:
+ print(e, file=sys.stderr)
+ return 1
+ except (oscerr.PackageExists, oscerr.PackageMissing, oscerr.WorkingCopyInconsistent) as e:
+ print(e.msg, file=sys.stderr)
+ return 1
+ except oscerr.PackageInternalError as e:
+ print('a package internal error occured\n' \
+ 'please file a bug and attach your current package working copy ' \
+ 'and the following traceback to it:', file=sys.stderr)
+ print(e.msg, file=sys.stderr)
+ traceback.print_exc(file=sys.stderr)
+ return 1
+ except oscerr.PackageError as e:
+ print(e.msg, file=sys.stderr)
+ return 1
+ except PackageError as e:
+ print('%s:' % e.fname, e.msg, file=sys.stderr)
+ return 1
+ except RPMError as e:
+ print(e, file=sys.stderr)
+ return 1
+ except SSLError as e:
+ print("SSL Error:", e, file=sys.stderr)
+ return 1
+ except SSLVerificationError as e:
+ print("Certificate Verification Error:", e, file=sys.stderr)
+ return 1
+ except NoSecureSSLError as e:
+ print(e, file=sys.stderr)
+ return 1
+ except CpioError as e:
+ print(e, file=sys.stderr)
+ return 1
+ except oscerr.OscBaseError as e:
+ print('*** Error:', e, file=sys.stderr)
+ return 1
+
+# vim: sw=4 et
diff --git a/osc/build.py b/osc/build.py
new file mode 100644
index 0000000..395ad12
--- /dev/null
+++ b/osc/build.py
@@ -0,0 +1,1110 @@
+# Copyright (C) 2006 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+import shutil
+
+try:
+ from urllib.parse import urlsplit
+ from urllib.request import URLError, HTTPError
+except ImportError:
+ #python 2.x
+ from urlparse import urlsplit
+ from urllib2 import URLError, HTTPError
+
+from tempfile import NamedTemporaryFile, mkdtemp
+from osc.fetch import *
+from osc.core import get_buildinfo, store_read_apiurl, store_read_project, store_read_package, meta_exists, quote_plus, get_buildconfig, is_package_dir
+from osc.core import get_binarylist, get_binary_file, run_external, raw_input
+from osc.util import rpmquery, debquery, archquery
+import osc.conf
+from . import oscerr
+import subprocess
+try:
+ from xml.etree import cElementTree as ET
+except ImportError:
+ import cElementTree as ET
+
+from .conf import config, cookiejar
+
+change_personality = {
+ 'i686': 'linux32',
+ 'i586': 'linux32',
+ 'i386': 'linux32',
+ 'ppc': 'powerpc32',
+ 's390': 's390',
+ 'sparc': 'linux32',
+ 'sparcv8': 'linux32',
+ }
+
+# FIXME: qemu_can_build should not be needed anymore since OBS 2.3
+qemu_can_build = [ 'armv4l', 'armv5el', 'armv5l', 'armv6l', 'armv7l', 'armv6el', 'armv6hl', 'armv7el', 'armv7hl', 'armv8el',
+ 'sh4', 'mips', 'mipsel',
+ 'ppc', 'ppc64',
+ 's390', 's390x',
+ 'sparc64v', 'sparcv9v', 'sparcv9', 'sparcv8', 'sparc',
+ 'hppa',
+ ]
+
+can_also_build = {
+ 'aarch64': ['aarch64'], # only needed due to used heuristics in build parameter evaluation
+ 'armv6l': [ 'armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el' ],
+ 'armv7l': [ 'armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv7el' ],
+ 'armv5el': [ 'armv4l', 'armv5l', 'armv5el' ], # not existing arch, just for compatibility
+ 'armv6el': [ 'armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el' ], # not existing arch, just for compatibility
+ 'armv6hl': [ 'armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el' ],
+ 'armv7el': [ 'armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv7el' ], # not existing arch, just for compatibility
+ 'armv7hl': [ 'armv7hl' ], # not existing arch, just for compatibility
+ 'armv8el': [ 'armv4l', 'armv5el', 'armv6el', 'armv7el', 'armv8el' ], # not existing arch, just for compatibility
+ 'armv8l': [ 'armv4l', 'armv5el', 'armv6el', 'armv7el', 'armv8el' ], # not existing arch, just for compatibility
+ 'armv5tel': [ 'armv4l', 'armv5el', 'armv5tel' ],
+ 's390x': ['s390' ],
+ 'ppc64': [ 'ppc', 'ppc64', 'ppc64p7', 'ppc64le' ],
+ 'ppc64le': [ 'ppc64le', 'ppc64' ],
+ 'i586': [ 'i386' ],
+ 'i686': [ 'i586', 'i386' ],
+ 'x86_64': ['i686', 'i586', 'i386' ],
+ 'sparc64': ['sparc64v', 'sparcv9v', 'sparcv9', 'sparcv8', 'sparc'],
+ 'parisc': ['hppa'],
+ }
+
+# real arch of this machine
+hostarch = os.uname()[4]
+if hostarch == 'i686': # FIXME
+ hostarch = 'i586'
+
+if hostarch == 'parisc':
+ hostarch = 'hppa'
+
+class Buildinfo:
+ """represent the contents of a buildinfo file"""
+
+ def __init__(self, filename, apiurl, buildtype = 'spec', localpkgs = []):
+ try:
+ tree = ET.parse(filename)
+ except:
+ print('could not parse the buildinfo:', file=sys.stderr)
+ print(open(filename).read(), file=sys.stderr)
+ sys.exit(1)
+
+ root = tree.getroot()
+
+ self.apiurl = apiurl
+
+ if root.find('error') != None:
+ sys.stderr.write('buildinfo is broken... it says:\n')
+ error = root.find('error').text
+ if error.startswith('unresolvable: '):
+ sys.stderr.write('unresolvable: ')
+ sys.stderr.write('\n '.join(error[14:].split(',')))
+ else:
+ sys.stderr.write(error)
+ sys.stderr.write('\n')
+ sys.exit(1)
+
+ if not (apiurl.startswith('https://') or apiurl.startswith('http://')):
+ raise URLError('invalid protocol for the apiurl: \'%s\'' % apiurl)
+
+ self.buildtype = buildtype
+ self.apiurl = apiurl
+
+ # are we building .rpm or .deb?
+ # XXX: shouldn't we deliver the type via the buildinfo?
+ self.pacsuffix = 'rpm'
+ if self.buildtype == 'dsc' or self.buildtype == 'collax':
+ self.pacsuffix = 'deb'
+ if self.buildtype == 'arch':
+ self.pacsuffix = 'arch'
+ if self.buildtype == 'livebuild':
+ self.pacsuffix = 'deb'
+
+ self.buildarch = root.find('arch').text
+ if root.find('hostarch') != None:
+ self.hostarch = root.find('hostarch').text
+ else:
+ self.hostarch = None
+ if root.find('release') != None:
+ self.release = root.find('release').text
+ else:
+ self.release = None
+ self.downloadurl = root.get('downloadurl')
+ self.debuginfo = 0
+ if root.find('debuginfo') != None:
+ try:
+ self.debuginfo = int(root.find('debuginfo').text)
+ except ValueError:
+ pass
+
+ self.deps = []
+ self.projects = {}
+ self.keys = []
+ self.prjkeys = []
+ self.pathes = []
+ for node in root.findall('bdep'):
+ p = Pac(node, self.buildarch, self.pacsuffix,
+ apiurl, localpkgs)
+ if p.project:
+ self.projects[p.project] = 1
+ self.deps.append(p)
+ for node in root.findall('path'):
+ self.pathes.append(node.get('project')+"/"+node.get('repository'))
+
+ self.vminstall_list = [ dep.name for dep in self.deps if dep.vminstall ]
+ self.preinstall_list = [ dep.name for dep in self.deps if dep.preinstall ]
+ self.runscripts_list = [ dep.name for dep in self.deps if dep.runscripts ]
+ self.noinstall_list = [ dep.name for dep in self.deps if dep.noinstall ]
+ self.installonly_list = [ dep.name for dep in self.deps if dep.installonly ]
+
+
+ def has_dep(self, name):
+ for i in self.deps:
+ if i.name == name:
+ return True
+ return False
+
+ def remove_dep(self, name):
+ # we need to iterate over all deps because if this a
+ # kiwi build the same package might appear multiple times
+ # NOTE: do not loop and remove items, the second same one would not get catched
+ self.deps = [i for i in self.deps if not i.name == name]
+
+
+class Pac:
+ """represent a package to be downloaded
+
+ We build a map that's later used to fill our URL templates
+ """
+ def __init__(self, node, buildarch, pacsuffix, apiurl, localpkgs = []):
+
+ self.mp = {}
+ for i in ['binary', 'package',
+ 'epoch', 'version', 'release', 'hdrmd5',
+ 'project', 'repository',
+ 'preinstall', 'vminstall', 'noinstall', 'installonly', 'runscripts',
+ ]:
+ self.mp[i] = node.get(i)
+
+ self.mp['buildarch'] = buildarch
+ self.mp['pacsuffix'] = pacsuffix
+
+ self.mp['arch'] = node.get('arch') or self.mp['buildarch']
+ self.mp['name'] = node.get('name') or self.mp['binary']
+
+ # this is not the ideal place to check if the package is a localdep or not
+ localdep = self.mp['name'] in localpkgs # and not self.mp['noinstall']
+ if not localdep and not (node.get('project') and node.get('repository')):
+ raise oscerr.APIError('incomplete information for package %s, may be caused by a broken project configuration.'
+ % self.mp['name'] )
+
+ if not localdep:
+ self.mp['extproject'] = node.get('project').replace(':', ':/')
+ self.mp['extrepository'] = node.get('repository').replace(':', ':/')
+ self.mp['repopackage'] = node.get('package') or '_repository'
+ self.mp['repoarch'] = node.get('repoarch') or self.mp['buildarch']
+
+ if pacsuffix == 'deb' and not (self.mp['name'] and self.mp['arch'] and self.mp['version']):
+ raise oscerr.APIError(
+ "buildinfo for package %s/%s/%s is incomplete"
+ % (self.mp['name'], self.mp['arch'], self.mp['version']))
+
+ self.mp['apiurl'] = apiurl
+
+ if pacsuffix == 'deb':
+ canonname = debquery.DebQuery.filename(self.mp['name'], self.mp['epoch'], self.mp['version'], self.mp['release'], self.mp['arch'])
+ elif pacsuffix == 'arch':
+ canonname = archquery.ArchQuery.filename(self.mp['name'], self.mp['epoch'], self.mp['version'], self.mp['release'], self.mp['arch'])
+ else:
+ canonname = rpmquery.RpmQuery.filename(self.mp['name'], self.mp['epoch'], self.mp['version'], self.mp['release'], self.mp['arch'])
+
+ self.mp['canonname'] = canonname
+ # maybe we should rename filename key to binary
+ self.mp['filename'] = node.get('binary') or canonname
+ if self.mp['repopackage'] == '_repository':
+ self.mp['repofilename'] = self.mp['name']
+ else:
+ # OBS 2.3 puts binary into product bdeps (noinstall ones)
+ self.mp['repofilename'] = self.mp['filename']
+
+ # make the content of the dictionary accessible as class attributes
+ self.__dict__.update(self.mp)
+
+
+ def makeurls(self, cachedir, urllist):
+
+ self.urllist = []
+
+ # build up local URL
+ # by using the urlgrabber with local urls, we basically build up a cache.
+ # the cache has no validation, since the package servers don't support etags,
+ # or if-modified-since, so the caching is simply name-based (on the assumption
+ # that the filename is suitable as identifier)
+ self.localdir = '%s/%s/%s/%s' % (cachedir, self.project, self.repository, self.arch)
+ self.fullfilename = os.path.join(self.localdir, self.canonname)
+ self.url_local = 'file://%s' % self.fullfilename
+
+ # first, add the local URL
+ self.urllist.append(self.url_local)
+
+ # remote URLs
+ for url in urllist:
+ self.urllist.append(url % self.mp)
+
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return "%s" % self.name
+
+
+
+def get_built_files(pacdir, buildtype):
+ if buildtype == 'spec':
+ b_built = subprocess.Popen(['find', os.path.join(pacdir, 'RPMS'),
+ '-name', '*.rpm'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ s_built = subprocess.Popen(['find', os.path.join(pacdir, 'SRPMS'),
+ '-name', '*.rpm'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ elif buildtype == 'kiwi':
+ b_built = subprocess.Popen(['find', os.path.join(pacdir, 'KIWI'),
+ '-type', 'f'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ s_built = ''
+ elif buildtype == 'dsc' or buildtype == 'collax':
+ b_built = subprocess.Popen(['find', os.path.join(pacdir, 'DEBS'),
+ '-name', '*.deb'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ s_built = subprocess.Popen(['find', os.path.join(pacdir, 'SOURCES.DEB'),
+ '-type', 'f'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ elif buildtype == 'arch':
+ b_built = subprocess.Popen(['find', os.path.join(pacdir, 'ARCHPKGS'),
+ '-name', '*.pkg.tar*'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ s_built = ''
+ elif buildtype == 'livebuild':
+ b_built = subprocess.Popen(['find', os.path.join(pacdir, 'OTHER'),
+ '-name', '*.iso*'],
+ stdout=subprocess.PIPE).stdout.read().strip()
+ s_built = ''
+ else:
+ print('WARNING: Unknown package type \'%s\'.' % buildtype, file=sys.stderr)
+ b_built = ''
+ s_built = ''
+ return s_built, b_built
+
+def get_repo(path):
+ """Walks up path looking for any repodata directories.
+
+ @param path path to a directory
+ @return str path to repository directory containing repodata directory
+ """
+ oldDirectory = None
+ currentDirectory = os.path.abspath(path)
+ repositoryDirectory = None
+
+ # while there are still parent directories
+ while currentDirectory != oldDirectory:
+ children = os.listdir(currentDirectory)
+
+ if "repodata" in children:
+ repositoryDirectory = currentDirectory
+ break
+
+ # ascend
+ oldDirectory = currentDirectory
+ currentDirectory = os.path.abspath(os.path.join(oldDirectory,
+ os.pardir))
+
+ return repositoryDirectory
+
+def get_prefer_pkgs(dirs, wanted_arch, type, cpio):
+ import glob
+ from .util import repodata, packagequery
+ paths = []
+ repositories = []
+
+ suffix = '*.rpm'
+ if type == 'dsc' or type == 'collax' or type == 'livebuild':
+ suffix = '*.deb'
+ elif type == 'arch':
+ suffix = '*.pkg.tar.xz'
+
+ for dir in dirs:
+ # check for repodata
+ repository = get_repo(dir)
+ if repository is None:
+ paths += glob.glob(os.path.join(os.path.abspath(dir), suffix))
+ else:
+ repositories.append(repository)
+
+ packageQueries = packagequery.PackageQueries(wanted_arch)
+
+ for repository in repositories:
+ repodataPackageQueries = repodata.queries(repository)
+
+ for packageQuery in repodataPackageQueries:
+ packageQueries.add(packageQuery)
+
+ for path in paths:
+ if path.endswith('.src.rpm') or path.endswith('.nosrc.rpm'):
+ continue
+ if path.endswith('.patch.rpm') or path.endswith('.delta.rpm'):
+ continue
+ if path.find('-debuginfo-') > 0:
+ continue
+ packageQuery = packagequery.PackageQuery.query(path)
+ packageQueries.add(packageQuery)
+
+ prefer_pkgs = dict((name, packageQuery.path())
+ for name, packageQuery in packageQueries.items())
+
+ depfile = create_deps(packageQueries.values())
+ cpio.add('deps', '\n'.join(depfile))
+ return prefer_pkgs
+
+
+def create_deps(pkgqs):
+ """
+ creates a list of dependencies which corresponds to build's internal
+ dependency file format
+ """
+ depfile = []
+ for p in pkgqs:
+ id = '%s.%s-0/0/0: ' % (p.name(), p.arch())
+ depfile.append('P:%s%s' % (id, ' '.join(p.provides())))
+ depfile.append('R:%s%s' % (id, ' '.join(p.requires())))
+ d = p.conflicts()
+ if d:
+ depfile.append('C:%s%s' % (id, ' '.join(d)))
+ d = p.obsoletes()
+ if d:
+ depfile.append('O:%s%s' % (id, ' '.join(d)))
+ depfile.append('I:%s%s-%s 0-%s' % (id, p.name(), p.evr(), p.arch()))
+ return depfile
+
+
+trustprompt = """Would you like to ...
+0 - quit (default)
+1 - always trust packages from '%(project)s'
+2 - trust packages just this time
+? """
+def check_trusted_projects(apiurl, projects):
+ trusted = config['api_host_options'][apiurl]['trusted_prj']
+ tlen = len(trusted)
+ for prj in projects:
+ if not prj in trusted:
+ print("\nThe build root needs packages from project '%s'." % prj)
+ print("Note that malicious packages can compromise the build result or even your system.")
+ r = raw_input(trustprompt % { 'project': prj })
+ if r == '1':
+ print("adding '%s' to ~/.oscrc: ['%s']['trusted_prj']" % (prj, apiurl))
+ trusted.append(prj)
+ elif r != '2':
+ print("Well, good good bye then :-)")
+ raise oscerr.UserAbort()
+
+ if tlen != len(trusted):
+ config['api_host_options'][apiurl]['trusted_prj'] = trusted
+ conf.config_set_option(apiurl, 'trusted_prj', ' '.join(trusted))
+
+def main(apiurl, opts, argv):
+
+ repo = argv[0]
+ arch = argv[1]
+ build_descr = argv[2]
+ xp = []
+ build_root = None
+ cache_dir = None
+ build_uid = ''
+ vm_type = config['build-type']
+ vm_telnet = None
+
+ build_descr = os.path.abspath(build_descr)
+ build_type = os.path.splitext(build_descr)[1][1:]
+ if os.path.basename(build_descr) == 'PKGBUILD':
+ build_type = 'arch'
+ if os.path.basename(build_descr) == 'build.collax':
+ build_type = 'collax'
+ if build_type not in ['spec', 'dsc', 'kiwi', 'arch', 'collax', 'livebuild']:
+ raise oscerr.WrongArgs(
+ 'Unknown build type: \'%s\'. Build description should end in .spec, .dsc, .kiwi or .livebuild.' \
+ % build_type)
+ if not os.path.isfile(build_descr):
+ raise oscerr.WrongArgs('Error: build description file named \'%s\' does not exist.' % build_descr)
+
+ buildargs = []
+ if not opts.userootforbuild:
+ buildargs.append('--norootforbuild')
+ if opts.clean:
+ buildargs.append('--clean')
+ if opts.noinit:
+ buildargs.append('--noinit')
+ if opts.nochecks:
+ buildargs.append('--no-checks')
+ if not opts.no_changelog:
+ buildargs.append('--changelog')
+ if opts.root:
+ build_root = opts.root
+ if opts.target:
+ buildargs.append('--target=%s' % opts.target)
+ if opts.threads:
+ buildargs.append('--threads=%s' % opts.threads)
+ if opts.jobs:
+ buildargs.append('--jobs=%s' % opts.jobs)
+ elif config['build-jobs'] > 1:
+ buildargs.append('--jobs=%s' % config['build-jobs'])
+ if opts.icecream or config['icecream'] != '0':
+ if opts.icecream:
+ num = opts.icecream
+ else:
+ num = config['icecream']
+
+ if int(num) > 0:
+ buildargs.append('--icecream=%s' % num)
+ xp.append('icecream')
+ xp.append('gcc-c++')
+ if opts.ccache:
+ buildargs.append('--ccache')
+ xp.append('ccache')
+ if opts.linksources:
+ buildargs.append('--linksources')
+ if opts.baselibs:
+ buildargs.append('--baselibs')
+ if opts.debuginfo:
+ buildargs.append('--debug')
+ if opts._with:
+ for o in opts._with:
+ buildargs.append('--with=%s' % o)
+ if opts.without:
+ for o in opts.without:
+ buildargs.append('--without=%s' % o)
+ if opts.define:
+ for o in opts.define:
+ buildargs.append('--define=%s' % o)
+ if config['build-uid']:
+ build_uid = config['build-uid']
+ if opts.build_uid:
+ build_uid = opts.build_uid
+ if build_uid:
+ buildidre = re.compile('^[0-9]{1,5}:[0-9]{1,5}$')
+ if build_uid == 'caller':
+ buildargs.append('--uid=%s:%s' % (os.getuid(), os.getgid()))
+ elif buildidre.match(build_uid):
+ buildargs.append('--uid=%s' % build_uid)
+ else:
+ print('Error: build-uid arg must be 2 colon separated numerics: "uid:gid" or "caller"', file=sys.stderr)
+ return 1
+ if opts.vm_type:
+ vm_type = opts.vm_type
+ if opts.vm_telnet:
+ vm_telnet = opts.vm_telnet
+ if opts.alternative_project:
+ prj = opts.alternative_project
+ pac = '_repository'
+ else:
+ prj = store_read_project(os.curdir)
+ if opts.local_package:
+ pac = '_repository'
+ else:
+ pac = store_read_package(os.curdir)
+ if opts.shell:
+ buildargs.append("--shell")
+
+ orig_build_root = config['build-root']
+ # make it possible to override configuration of the rc file
+ for var in ['OSC_PACKAGECACHEDIR', 'OSC_SU_WRAPPER', 'OSC_BUILD_ROOT']:
+ val = os.getenv(var)
+ if val:
+ if var.startswith('OSC_'): var = var[4:]
+ var = var.lower().replace('_', '-')
+ if var in config:
+ print('Overriding config value for %s=\'%s\' with \'%s\'' % (var, config[var], val))
+ config[var] = val
+
+ pacname = pac
+ if pacname == '_repository':
+ if not opts.local_package:
+ try:
+ pacname = store_read_package(os.curdir)
+ except oscerr.NoWorkingCopy:
+ opts.local_package = True
+ if opts.local_package:
+ pacname = os.path.splitext(build_descr)[0]
+ apihost = urlsplit(apiurl)[1]
+ if not build_root:
+ build_root = config['build-root']
+ if build_root == orig_build_root:
+ # ENV var was not set
+ build_root = config['api_host_options'][apiurl].get('build-root', build_root)
+ try:
+ build_root = build_root % {'repo': repo, 'arch': arch,
+ 'project': prj, 'package': pacname, 'apihost': apihost}
+ except:
+ pass
+
+ cache_dir = config['packagecachedir'] % {'apihost': apihost}
+
+ extra_pkgs = []
+ if not opts.extra_pkgs:
+ extra_pkgs = config['extra-pkgs']
+ elif opts.extra_pkgs != ['']:
+ extra_pkgs = opts.extra_pkgs
+
+ if xp:
+ extra_pkgs += xp
+
+ prefer_pkgs = {}
+ build_descr_data = open(build_descr).read()
+
+ # XXX: dirty hack but there's no api to provide custom defines
+ if opts.without:
+ s = ''
+ for i in opts.without:
+ s += "%%define _without_%s 1\n" % i
+ build_descr_data = s + build_descr_data
+ if opts._with:
+ s = ''
+ for i in opts._with:
+ s += "%%define _with_%s 1\n" % i
+ build_descr_data = s + build_descr_data
+ if opts.define:
+ s = ''
+ for i in opts.define:
+ s += "%%define %s\n" % i
+ build_descr_data = s + build_descr_data
+
+ cpiodata = None
+ servicefile = os.path.join(os.path.dirname(build_descr), "_service")
+ if not os.path.isfile(servicefile):
+ servicefile = os.path.join(os.path.dirname(build_descr), "_service")
+ if not os.path.isfile(servicefile):
+ servicefile = None
+ else:
+ print('Using local _service file')
+ buildenvfile = os.path.join(os.path.dirname(build_descr), "_buildenv." + repo + "." + arch)
+ if not os.path.isfile(buildenvfile):
+ buildenvfile = os.path.join(os.path.dirname(build_descr), "_buildenv")
+ if not os.path.isfile(buildenvfile):
+ buildenvfile = None
+ else:
+ print('Using local buildenv file: %s' % os.path.basename(buildenvfile))
+ if buildenvfile or servicefile:
+ from .util import cpio
+ if not cpiodata:
+ cpiodata = cpio.CpioWrite()
+
+ if opts.prefer_pkgs:
+ print('Scanning the following dirs for local packages: %s' % ', '.join(opts.prefer_pkgs))
+ from .util import cpio
+ if not cpiodata:
+ cpiodata = cpio.CpioWrite()
+ prefer_pkgs = get_prefer_pkgs(opts.prefer_pkgs, arch, build_type, cpiodata)
+
+ if cpiodata:
+ cpiodata.add(os.path.basename(build_descr), build_descr_data)
+ # buildenv must come last for compatibility reasons...
+ if buildenvfile:
+ cpiodata.add("buildenv", open(buildenvfile).read())
+ if servicefile:
+ cpiodata.add("_service", open(servicefile).read())
+ build_descr_data = cpiodata.get()
+
+ # special handling for overlay and rsync-src/dest
+ specialcmdopts = []
+ if opts.rsyncsrc or opts.rsyncdest :
+ if not opts.rsyncsrc or not opts.rsyncdest:
+ raise oscerr.WrongOptions('When using --rsync-{src,dest} both parameters have to be specified.')
+ myrsyncsrc = os.path.abspath(os.path.expanduser(os.path.expandvars(opts.rsyncsrc)))
+ if not os.path.isdir(myrsyncsrc):
+ raise oscerr.WrongOptions('--rsync-src %s is no valid directory!' % opts.rsyncsrc)
+ # can't check destination - its in the target chroot ;) - but we can check for sanity
+ myrsyncdest = os.path.expandvars(opts.rsyncdest)
+ if not os.path.isabs(myrsyncdest):
+ raise oscerr.WrongOptions('--rsync-dest %s is no absolute path (starting with \'/\')!' % opts.rsyncdest)
+ specialcmdopts = ['--rsync-src='+myrsyncsrc, '--rsync-dest='+myrsyncdest]
+ if opts.overlay:
+ myoverlay = os.path.abspath(os.path.expanduser(os.path.expandvars(opts.overlay)))
+ if not os.path.isdir(myoverlay):
+ raise oscerr.WrongOptions('--overlay %s is no valid directory!' % opts.overlay)
+ specialcmdopts += ['--overlay='+myoverlay]
+
+ bi_file = None
+ bc_file = None
+ bi_filename = '_buildinfo-%s-%s.xml' % (repo, arch)
+ bc_filename = '_buildconfig-%s-%s' % (repo, arch)
+ if is_package_dir('.') and os.access(osc.core.store, os.W_OK):
+ bi_filename = os.path.join(os.getcwd(), osc.core.store, bi_filename)
+ bc_filename = os.path.join(os.getcwd(), osc.core.store, bc_filename)
+ elif not os.access('.', os.W_OK):
+ bi_file = NamedTemporaryFile(prefix=bi_filename)
+ bi_filename = bi_file.name
+ bc_file = NamedTemporaryFile(prefix=bc_filename)
+ bc_filename = bc_file.name
+ else:
+ bi_filename = os.path.abspath(bi_filename)
+ bc_filename = os.path.abspath(bc_filename)
+
+ try:
+ if opts.noinit:
+ if not os.path.isfile(bi_filename):
+ raise oscerr.WrongOptions('--noinit is not possible, no local buildinfo file')
+ print('Use local \'%s\' file as buildinfo' % bi_filename)
+ if not os.path.isfile(bc_filename):
+ raise oscerr.WrongOptions('--noinit is not possible, no local buildconfig file')
+ print('Use local \'%s\' file as buildconfig' % bc_filename)
+ elif opts.offline:
+ if not os.path.isfile(bi_filename):
+ raise oscerr.WrongOptions('--offline is not possible, no local buildinfo file')
+ print('Use local \'%s\' file as buildinfo' % bi_filename)
+ if not os.path.isfile(bc_filename):
+ raise oscerr.WrongOptions('--offline is not possible, no local buildconfig file')
+ else:
+ print('Getting buildinfo from server and store to %s' % bi_filename)
+ bi_text = ''.join(get_buildinfo(apiurl,
+ prj,
+ pac,
+ repo,
+ arch,
+ specfile=build_descr_data,
+ addlist=extra_pkgs))
+ if not bi_file:
+ bi_file = open(bi_filename, 'w')
+ # maybe we should check for errors before saving the file
+ bi_file.write(bi_text)
+ bi_file.flush()
+ print('Getting buildconfig from server and store to %s' % bc_filename)
+ bc = get_buildconfig(apiurl, prj, repo)
+ if not bc_file:
+ bc_file = open(bc_filename, 'w')
+ bc_file.write(bc)
+ bc_file.flush()
+ except HTTPError as e:
+ if e.code == 404:
+ # check what caused the 404
+ if meta_exists(metatype='prj', path_args=(quote_plus(prj), ),
+ template_args=None, create_new=False, apiurl=apiurl):
+ pkg_meta_e = None
+ try:
+ # take care, not to run into double trouble.
+ pkg_meta_e = meta_exists(metatype='pkg', path_args=(quote_plus(prj),
+ quote_plus(pac)), template_args=None, create_new=False,
+ apiurl=apiurl)
+ except:
+ pass
+
+ if pkg_meta_e:
+ print('ERROR: Either wrong repo/arch as parameter or a parse error of .spec/.dsc/.kiwi file due to syntax error', file=sys.stderr)
+ else:
+ print('The package \'%s\' does not exist - please ' \
+ 'rerun with \'--local-package\'' % pac, file=sys.stderr)
+ else:
+ print('The project \'%s\' does not exist - please ' \
+ 'rerun with \'--alternative-project <alternative_project>\'' % prj, file=sys.stderr)
+ sys.exit(1)
+ else:
+ raise
+
+ bi = Buildinfo(bi_filename, apiurl, build_type, list(prefer_pkgs.keys()))
+
+ if bi.debuginfo and not (opts.disable_debuginfo or '--debug' in buildargs):
+ buildargs.append('--debug')
+
+ if opts.release:
+ bi.release = opts.release
+
+ if bi.release:
+ buildargs.append('--release=%s' % bi.release)
+
+ # real arch of this machine
+ # vs.
+ # arch we are supposed to build for
+ if bi.hostarch != None:
+ if hostarch != bi.hostarch and not bi.hostarch in can_also_build.get(hostarch, []):
+ print('Error: hostarch \'%s\' is required.' % (bi.hostarch), file=sys.stderr)
+ return 1
+ elif hostarch != bi.buildarch:
+ if not bi.buildarch in can_also_build.get(hostarch, []):
+ # OBSOLETE: qemu_can_build should not be needed anymore since OBS 2.3
+ if vm_type != "emulator" and not bi.buildarch in qemu_can_build:
+ print('Error: hostarch \'%s\' cannot build \'%s\'.' % (hostarch, bi.buildarch), file=sys.stderr)
+ return 1
+ print('WARNING: It is guessed to build on hostarch \'%s\' for \'%s\' via QEMU.' % (hostarch, bi.buildarch), file=sys.stderr)
+
+ rpmlist_prefers = []
+ if prefer_pkgs:
+ print('Evaluating preferred packages')
+ for name, path in prefer_pkgs.items():
+ if bi.has_dep(name):
+ # We remove a preferred package from the buildinfo, so that the
+ # fetcher doesn't take care about them.
+ # Instead, we put it in a list which is appended to the rpmlist later.
+ # At the same time, this will make sure that these packages are
+ # not verified.
+ bi.remove_dep(name)
+ rpmlist_prefers.append((name, path))
+ print(' - %s (%s)' % (name, path))
+
+ print('Updating cache of required packages')
+
+ urllist = []
+ if not opts.download_api_only:
+ # transform 'url1, url2, url3' form into a list
+ if 'urllist' in config:
+ if isinstance(config['urllist'], str):
+ re_clist = re.compile('[, ]+')
+ urllist = [ i.strip() for i in re_clist.split(config['urllist'].strip()) ]
+ else:
+ urllist = config['urllist']
+
+ # OBS 1.5 and before has no downloadurl defined in buildinfo
+ if bi.downloadurl:
+ urllist.append(bi.downloadurl + '/%(extproject)s/%(extrepository)s/%(arch)s/%(filename)s')
+ if opts.disable_cpio_bulk_download:
+ urllist.append( '%(apiurl)s/build/%(project)s/%(repository)s/%(repoarch)s/%(repopackage)s/%(repofilename)s' )
+
+ fetcher = Fetcher(cache_dir,
+ urllist = urllist,
+ api_host_options = config['api_host_options'],
+ offline = opts.noinit or opts.offline,
+ http_debug = config['http_debug'],
+ enable_cpio = not opts.disable_cpio_bulk_download,
+ cookiejar=cookiejar)
+
+ if not opts.trust_all_projects:
+ # implicitly trust the project we are building for
+ check_trusted_projects(apiurl, [ i for i in bi.projects.keys() if not i == prj ])
+
+ # now update the package cache
+ fetcher.run(bi)
+
+ old_pkg_dir = None
+ if opts.oldpackages:
+ old_pkg_dir = opts.oldpackages
+ if not old_pkg_dir.startswith('/') and not opts.offline:
+ data = [ prj, pacname, repo, arch]
+ if old_pkg_dir == '_link':
+ p = osc.core.findpacs(os.curdir)[0]
+ if not p.islink():
+ raise oscerr.WrongOptions('package is not a link')
+ data[0] = p.linkinfo.project
+ data[1] = p.linkinfo.package
+ repos = osc.core.get_repositories_of_project(apiurl, data[0])
+ # hack for links to e.g. Factory
+ if not data[2] in repos and 'standard' in repos:
+ data[2] = 'standard'
+ elif old_pkg_dir != '' and old_pkg_dir != '_self':
+ a = old_pkg_dir.split('/')
+ for i in range(0, len(a)):
+ data[i] = a[i]
+
+ destdir = os.path.join(cache_dir, data[0], data[2], data[3])
+ old_pkg_dir = None
+ try:
+ print("Downloading previous build from %s ..." % '/'.join(data))
+ binaries = get_binarylist(apiurl, data[0], data[2], data[3], package=data[1], verbose=True)
+ except Exception as e:
+ print("Error: failed to get binaries: %s" % str(e))
+ binaries = []
+
+ if binaries:
+ class mytmpdir:
+ """ temporary directory that removes itself"""
+ def __init__(self, *args, **kwargs):
+ self.name = mkdtemp(*args, **kwargs)
+ _rmtree = staticmethod(shutil.rmtree)
+ def cleanup(self):
+ self._rmtree(self.name)
+ def __del__(self):
+ self.cleanup()
+ def __exit__(self):
+ self.cleanup()
+ def __str__(self):
+ return self.name
+
+ old_pkg_dir = mytmpdir(prefix='.build.oldpackages', dir=os.path.abspath(os.curdir))
+ if not os.path.exists(destdir):
+ os.makedirs(destdir)
+ for i in binaries:
+ fname = os.path.join(destdir, i.name)
+ os.symlink(fname, os.path.join(str(old_pkg_dir), i.name))
+ if os.path.exists(fname):
+ st = os.stat(fname)
+ if st.st_mtime == i.mtime and st.st_size == i.size:
+ continue
+ get_binary_file(apiurl,
+ data[0],
+ data[2], data[3],
+ i.name,
+ package = data[1],
+ target_filename = fname,
+ target_mtime = i.mtime,
+ progress_meter = True)
+
+ if old_pkg_dir != None:
+ buildargs.append('--oldpackages=%s' % old_pkg_dir)
+
+ # Make packages from buildinfo available as repos for kiwi
+ if build_type == 'kiwi':
+ if os.path.exists('repos'):
+ shutil.rmtree('repos')
+ os.mkdir('repos')
+ for i in bi.deps:
+ if not i.extproject:
+ # remove
+ bi.deps.remove(i)
+ continue
+ # project
+ pdir = str(i.extproject).replace(':/', ':')
+ # repo
+ rdir = str(i.extrepository).replace(':/', ':')
+ # arch
+ adir = i.repoarch
+ # project/repo
+ prdir = "repos/"+pdir+"/"+rdir
+ # project/repo/arch
+ pradir = prdir+"/"+adir
+ # source fullfilename
+ sffn = i.fullfilename
+ filename = sffn.split("/")[-1]
+ # target fullfilename
+ tffn = pradir+"/"+filename
+ if not os.path.exists(os.path.join(pradir)):
+ os.makedirs(os.path.join(pradir))
+ if not os.path.exists(tffn):
+ print("Using package: "+sffn)
+ if opts.linksources:
+ os.link(sffn, tffn)
+ else:
+ os.symlink(sffn, tffn)
+ if prefer_pkgs:
+ for name, path in prefer_pkgs.items():
+ if name == filename:
+ print("Using prefered package: " + path + "/" + filename)
+ os.unlink(tffn)
+ if opts.linksources:
+ os.link(path + "/" + filename, tffn)
+ else:
+ os.symlink(path + "/" + filename, tffn)
+ # Is a obsrepositories tag used?
+ try:
+ tree = ET.parse(build_descr)
+ except:
+ print('could not parse the kiwi file:', file=sys.stderr)
+ print(open(build_descr).read(), file=sys.stderr)
+ sys.exit(1)
+ root = tree.getroot()
+ # product
+ for xml in root.findall('instsource'):
+ if xml.find('instrepo').find('source').get('path') == 'obsrepositories:/':
+ print("obsrepositories:/ for product builds is not yet supported in osc!")
+ sys.exit(1)
+ # appliance
+ expand_obsrepos=None
+ for xml in root.findall('repository'):
+ if xml.find('source').get('path') == 'obsrepositories:/':
+ expand_obsrepos=True
+ if expand_obsrepos:
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--ignore-repos')
+ for xml in root.findall('repository'):
+ if xml.find('source').get('path') == 'obsrepositories:/':
+ for path in bi.pathes:
+ if not os.path.isdir("repos/"+path):
+ continue
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repo')
+ buildargs.append('--kiwi-parameter')
+ buildargs.append("repos/"+path)
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repotype')
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('rpm-md')
+ if xml.get('priority'):
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repoprio='+xml.get('priority'))
+ else:
+ m = re.match(r"obs://[^/]+/([^/]+)/(\S+)", xml.find('source').get('path'))
+ if not m:
+ # short path without obs instance name
+ m = re.match(r"obs://([^/]+)/(.+)", xml.find('source').get('path'))
+ project=m.group(1).replace(":", ":/")
+ repo=m.group(2)
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repo')
+ buildargs.append('--kiwi-parameter')
+ buildargs.append("repos/"+project+"/"+repo)
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repotype')
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('rpm-md')
+ if xml.get('priority'):
+ buildargs.append('--kiwi-parameter')
+ buildargs.append('--add-repopriority='+xml.get('priority'))
+
+ if vm_type == "xen" or vm_type == "kvm" or vm_type == "lxc":
+ print('Skipping verification of package signatures due to secure VM build')
+ elif bi.pacsuffix == 'rpm':
+ if opts.no_verify:
+ print('Skipping verification of package signatures')
+ else:
+ print('Verifying integrity of cached packages')
+ verify_pacs(bi)
+ elif bi.pacsuffix == 'deb':
+ if opts.no_verify or opts.noinit:
+ print('Skipping verification of package signatures')
+ else:
+ print('WARNING: deb packages get not verified, they can compromise your system !')
+ else:
+ print('WARNING: unknown packages get not verified, they can compromise your system !')
+
+ for i in bi.deps:
+ if i.hdrmd5:
+ from .util import packagequery
+ hdrmd5 = packagequery.PackageQuery.queryhdrmd5(i.fullfilename)
+ if not hdrmd5:
+ print("Error: cannot get hdrmd5 for %s" % i.fullfilename)
+ sys.exit(1)
+ if hdrmd5 != i.hdrmd5:
+ print("Error: hdrmd5 mismatch for %s: %s != %s" % (i.fullfilename, hdrmd5, i.hdrmd5))
+ sys.exit(1)
+
+ print('Writing build configuration')
+
+ if build_type == 'kiwi':
+ rpmlist = [ '%s %s\n' % (i.name, i.fullfilename) for i in bi.deps if not i.noinstall ]
+ else:
+ rpmlist = [ '%s %s\n' % (i.name, i.fullfilename) for i in bi.deps ]
+ rpmlist += [ '%s %s\n' % (i[0], i[1]) for i in rpmlist_prefers ]
+
+ rpmlist.append('preinstall: ' + ' '.join(bi.preinstall_list) + '\n')
+ rpmlist.append('vminstall: ' + ' '.join(bi.vminstall_list) + '\n')
+ rpmlist.append('runscripts: ' + ' '.join(bi.runscripts_list) + '\n')
+ if build_type != 'kiwi' and bi.noinstall_list:
+ rpmlist.append('noinstall: ' + ' '.join(bi.noinstall_list) + '\n')
+ if build_type != 'kiwi' and bi.installonly_list:
+ rpmlist.append('installonly: ' + ' '.join(bi.installonly_list) + '\n')
+
+ rpmlist_file = NamedTemporaryFile(prefix='rpmlist.')
+ rpmlist_filename = rpmlist_file.name
+ rpmlist_file.writelines(rpmlist)
+ rpmlist_file.flush()
+
+ subst = { 'repo': repo, 'arch': arch, 'project' : prj, 'package' : pacname }
+ vm_options = []
+ # XXX check if build-device present
+ my_build_device = ''
+ if config['build-device']:
+ my_build_device = config['build-device'] % subst
+ else:
+ # obs worker uses /root here but that collides with the
+ # /root directory if the build root was used without vm
+ # before
+ my_build_device = build_root + '/img'
+
+ need_root = True
+ if vm_type:
+ if config['build-swap']:
+ my_build_swap = config['build-swap'] % subst
+ else:
+ my_build_swap = build_root + '/swap'
+
+ vm_options = [ '--vm-type=%s' % vm_type ]
+ if vm_telnet:
+ vm_options += [ '--vm-telnet=' + vm_telnet ]
+ if config['build-memory']:
+ vm_options += [ '--memory=' + config['build-memory'] ]
+ if vm_type != 'lxc':
+ vm_options += [ '--vm-disk=' + my_build_device ]
+ vm_options += [ '--vm-swap=' + my_build_swap ]
+ vm_options += [ '--logfile=%s/.build.log' % build_root ]
+ if vm_type == 'kvm':
+ if os.access(build_root, os.W_OK) and os.access('/dev/kvm', os.W_OK):
+ # so let's hope there's also an fstab entry
+ need_root = False
+ if config['build-kernel']:
+ vm_options += [ '--vm-kernel=' + config['build-kernel'] ]
+ if config['build-initrd']:
+ vm_options += [ '--vm-initrd=' + config['build-initrd'] ]
+
+ build_root += '/.mount'
+
+ if config['build-memory']:
+ vm_options += [ '--memory=' + config['build-memory'] ]
+ if config['build-vmdisk-rootsize']:
+ vm_options += [ '--vmdisk-rootsize=' + config['build-vmdisk-rootsize'] ]
+ if config['build-vmdisk-swapsize']:
+ vm_options += [ '--vmdisk-swapsize=' + config['build-vmdisk-swapsize'] ]
+ if config['build-vmdisk-filesystem']:
+ vm_options += [ '--vmdisk-filesystem=' + config['build-vmdisk-filesystem'] ]
+ if config['build-vm-user']:
+ vm_options += [ '--vm-user=' + config['build-vm-user'] ]
+
+
+ if opts.preload:
+ print("Preload done for selected repo/arch.")
+ sys.exit(0)
+
+ print('Running build')
+ cmd = [ config['build-cmd'], '--root='+build_root,
+ '--rpmlist='+rpmlist_filename,
+ '--dist='+bc_filename,
+ '--arch='+bi.buildarch ]
+ cmd += specialcmdopts + vm_options + buildargs
+ cmd += [ build_descr ]
+
+ if need_root:
+ sucmd = config['su-wrapper'].split()
+ if sucmd[0] == 'su':
+ if sucmd[-1] == '-c':
+ sucmd.pop()
+ cmd = sucmd + ['-s', cmd[0], 'root', '--' ] + cmd[1:]
+ else:
+ cmd = sucmd + cmd
+
+ # change personality, if needed
+ if hostarch != bi.buildarch and bi.buildarch in change_personality:
+ cmd = [ change_personality[bi.buildarch] ] + cmd
+
+ try:
+ rc = run_external(cmd[0], *cmd[1:])
+ if rc:
+ print()
+ print('The buildroot was:', build_root)
+ sys.exit(rc)
+ except KeyboardInterrupt as i:
+ print("keyboard interrupt, killing build ...")
+ cmd.append('--kill')
+ run_external(cmd[0], *cmd[1:])
+ raise i
+
+ pacdir = os.path.join(build_root, '.build.packages')
+ if os.path.islink(pacdir):
+ pacdir = os.readlink(pacdir)
+ pacdir = os.path.join(build_root, pacdir)
+
+ if os.path.exists(pacdir):
+ (s_built, b_built) = get_built_files(pacdir, bi.buildtype)
+
+ print()
+ if s_built: print(s_built)
+ print()
+ print(b_built)
+
+ if opts.keep_pkgs:
+ for i in b_built.splitlines() + s_built.splitlines():
+ shutil.copy2(i, os.path.join(opts.keep_pkgs, os.path.basename(i)))
+
+ if bi_file:
+ bi_file.close()
+ if bc_file:
+ bc_file.close()
+ rpmlist_file.close()
+
+# vim: sw=4 et
diff --git a/osc/checker.py b/osc/checker.py
new file mode 100644
index 0000000..f80a0da
--- /dev/null
+++ b/osc/checker.py
@@ -0,0 +1,122 @@
+from __future__ import print_function
+
+from tempfile import mkdtemp
+import os
+from shutil import rmtree
+import rpm
+import base64
+
+class KeyError(Exception):
+ def __init__(self, key, *args):
+ Exception.__init__(self)
+ self.args = args
+ self.key = key
+ def __str__(self):
+ return ''+self.key+' :'+' '.join(self.args)
+
+class Checker:
+ def __init__(self):
+ self.dbdir = mkdtemp(prefix='oscrpmdb')
+ self.imported = {}
+ rpm.addMacro('_dbpath', self.dbdir)
+ self.ts = rpm.TransactionSet()
+ self.ts.initDB()
+ self.ts.openDB()
+ self.ts.setVSFlags(0)
+ #self.ts.Debug(1)
+
+ def readkeys(self, keys=[]):
+ rpm.addMacro('_dbpath', self.dbdir)
+ for key in keys:
+ try:
+ self.readkey(key)
+ except KeyError as e:
+ print(e)
+
+ if not len(self.imported):
+ raise KeyError('', "no key imported")
+
+ rpm.delMacro("_dbpath")
+
+# python is an idiot
+# def __del__(self):
+# self.cleanup()
+
+ def cleanup(self):
+ self.ts.closeDB()
+ rmtree(self.dbdir)
+
+ def readkey(self, file):
+ if file in self.imported:
+ return
+
+ fd = open(file, "r")
+ line = fd.readline()
+ if line and line[0:14] == "-----BEGIN PGP":
+ line = fd.readline()
+ while line and line != "\n":
+ line = fd.readline()
+ if not line:
+ raise KeyError(file, "not a pgp public key")
+ else:
+ raise KeyError(file, "not a pgp public key")
+
+ key = ''
+ line = fd.readline()
+ crc = None
+ while line:
+ if line[0:12] == "-----END PGP":
+ break
+ line = line.rstrip()
+ if (line[0] == '='):
+ crc = line[1:]
+ line = fd.readline()
+ break
+ else:
+ key += line
+ line = fd.readline()
+ fd.close()
+ if not line or line[0:12] != "-----END PGP":
+ raise KeyError(file, "not a pgp public key")
+
+ # TODO: compute and compare CRC, see RFC 2440
+
+ bkey = base64.b64decode(key)
+
+ r = self.ts.pgpImportPubkey(bkey)
+ if r != 0:
+ raise KeyError(file, "failed to import pubkey")
+ self.imported[file] = 1
+
+ def check(self, pkg):
+ # avoid errors on non rpm
+ if pkg[-4:] != '.rpm':
+ return
+ fd = None
+ try:
+ fd = os.open(pkg, os.O_RDONLY)
+ hdr = self.ts.hdrFromFdno(fd)
+ finally:
+ if fd is not None:
+ os.close(fd)
+
+if __name__ == "__main__":
+ import sys
+ keyfiles = []
+ pkgs = []
+ for arg in sys.argv[1:]:
+ if arg[-4:] == '.rpm':
+ pkgs.append(arg)
+ else:
+ keyfiles.append(arg)
+
+ checker = Checker()
+ try:
+ checker.readkeys(keyfiles)
+ for pkg in pkgs:
+ checker.check(pkg)
+ except Exception as e:
+ checker.cleanup()
+ raise e
+
+# vim: sw=4 et
diff --git a/osc/cmdln.py b/osc/cmdln.py
new file mode 100644
index 0000000..da49ce6
--- /dev/null
+++ b/osc/cmdln.py
@@ -0,0 +1,1600 @@
+# Copyright (c) 2002-2005 ActiveState Corp.
+# License: MIT (see LICENSE.txt for license details)
+# Author: Trent Mick (TrentM@ActiveState.com)
+# Home: http://trentm.com/projects/cmdln/
+
+from __future__ import print_function
+
+"""An improvement on Python's standard cmd.py module.
+
+As with cmd.py, this module provides "a simple framework for writing
+line-oriented command intepreters." This module provides a 'RawCmdln'
+class that fixes some design flaws in cmd.Cmd, making it more scalable
+and nicer to use for good 'cvs'- or 'svn'-style command line interfaces
+or simple shells. And it provides a 'Cmdln' class that add
+optparse-based option processing. Basically you use it like this:
+
+ import cmdln
+
+ class MySVN(cmdln.Cmdln):
+ name = "svn"
+
+ @cmdln.alias('stat', 'st')
+ @cmdln.option('-v', '--verbose', action='store_true'
+ help='print verbose information')
+ def do_status(self, subcmd, opts, *paths):
+ print "handle 'svn status' command"
+
+ #...
+
+ if __name__ == "__main__":
+ shell = MySVN()
+ retval = shell.main()
+ sys.exit(retval)
+
+See the README.txt or <http://trentm.com/projects/cmdln/> for more
+details.
+"""
+
+__revision__ = "$Id: cmdln.py 1666 2007-05-09 03:13:03Z trentm $"
+__version_info__ = (1, 0, 0)
+__version__ = '.'.join(map(str, __version_info__))
+
+import os
+import re
+import cmd
+import optparse
+import sys
+from pprint import pprint
+from datetime import date
+
+# this is python 2.x style
+def introspect_handler_2(handler):
+ # Extract the introspection bits we need.
+ func = handler.im_func
+ if func.func_defaults:
+ func_defaults = func.func_defaults
+ else:
+ func_defaults = []
+ return \
+ func_defaults, \
+ func.func_code.co_argcount, \
+ func.func_code.co_varnames, \
+ func.func_code.co_flags, \
+ func
+
+def introspect_handler_3(handler):
+ defaults = handler.__defaults__
+ if not defaults:
+ defaults = []
+ else:
+ defaults = list(handler.__defaults__)
+ return \
+ defaults, \
+ handler.__code__.co_argcount, \
+ handler.__code__.co_varnames, \
+ handler.__code__.co_flags, \
+ handler.__func__
+
+if sys.version_info[0] == 2:
+ introspect_handler = introspect_handler_2
+ bytes = lambda x, *args: x
+else:
+ introspect_handler = introspect_handler_3
+
+
+#---- globals
+
+LOOP_ALWAYS, LOOP_NEVER, LOOP_IF_EMPTY = range(3)
+
+# An unspecified optional argument when None is a meaningful value.
+_NOT_SPECIFIED = ("Not", "Specified")
+
+# Pattern to match a TypeError message from a call that
+# failed because of incorrect number of arguments (see
+# Python/getargs.c).
+_INCORRECT_NUM_ARGS_RE = re.compile(
+ r"(takes [\w ]+ )(\d+)( arguments? \()(\d+)( given\))")
+
+# Static bits of man page
+MAN_HEADER = r""".TH %(ucname)s "1" "%(date)s" "%(name)s %(version)s" "User Commands"
+.SH NAME
+%(name)s \- Program to do useful things.
+.SH SYNOPSIS
+.B %(name)s
+[\fIGLOBALOPTS\fR] \fISUBCOMMAND \fR[\fIOPTS\fR] [\fIARGS\fR...]
+.br
+.B %(name)s
+\fIhelp SUBCOMMAND\fR
+.SH DESCRIPTION
+"""
+MAN_COMMANDS_HEADER = r"""
+.SS COMMANDS
+"""
+MAN_OPTIONS_HEADER = r"""
+.SS GLOBAL OPTIONS
+"""
+MAN_FOOTER = r"""
+.SH AUTHOR
+This man page is automatically generated.
+"""
+
+#---- exceptions
+
+class CmdlnError(Exception):
+ """A cmdln.py usage error."""
+ def __init__(self, msg):
+ self.msg = msg
+ def __str__(self):
+ return self.msg
+
+class CmdlnUserError(Exception):
+ """An error by a user of a cmdln-based tool/shell."""
+ pass
+
+
+
+#---- public methods and classes
+
+def alias(*aliases):
+ """Decorator to add aliases for Cmdln.do_* command handlers.
+
+ Example:
+ class MyShell(cmdln.Cmdln):
+ @cmdln.alias("!", "sh")
+ def do_shell(self, argv):
+ #...implement 'shell' command
+ """
+ def decorate(f):
+ if not hasattr(f, "aliases"):
+ f.aliases = []
+ f.aliases += aliases
+ return f
+ return decorate
+
+MAN_REPLACES = [
+ (re.compile(r'(^|[ \t\[\'\|])--([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\-\2\-\3\-\4\-\5\-\6'),
+ (re.compile(r'(^|[ \t\[\'\|])--([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\-\2\-\3\-\4\-\5'),
+ (re.compile(r'(^|[ \t\[\'\|])--([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\-\2\-\3\-\4'),
+ (re.compile(r'(^|[ \t\[\'\|])-([^/ \t/,-]*)-([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\2\-\3\-\4'),
+ (re.compile(r'(^|[ \t\[\'\|])--([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\-\2\-\3'),
+ (re.compile(r'(^|[ \t\[\'\|])-([^/ \t/,-]*)-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\2\-\3'),
+ (re.compile(r'(^|[ \t\[\'\|])--([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\-\2'),
+ (re.compile(r'(^|[ \t\[\'\|])-([^/ \t/,\|-]*)(?=$|[ \t=\]\'/,\|])'), r'\1\-\2'),
+ (re.compile(r"^'"), r" '"),
+ ]
+
+def man_escape(text):
+ '''
+ Escapes text to be included in man page.
+
+ For now it only escapes dashes in command line options.
+ '''
+ for repl in MAN_REPLACES:
+ text = repl[0].sub(repl[1], text)
+ return text
+
+class RawCmdln(cmd.Cmd):
+ """An improved (on cmd.Cmd) framework for building multi-subcommand
+ scripts (think "svn" & "cvs") and simple shells (think "pdb" and
+ "gdb").
+
+ A simple example:
+
+ import cmdln
+
+ class MySVN(cmdln.RawCmdln):
+ name = "svn"
+
+ @cmdln.aliases('stat', 'st')
+ def do_status(self, argv):
+ print "handle 'svn status' command"
+
+ if __name__ == "__main__":
+ shell = MySVN()
+ retval = shell.main()
+ sys.exit(retval)
+
+ See <http://trentm.com/projects/cmdln> for more information.
+ """
+ name = None # if unset, defaults basename(sys.argv[0])
+ prompt = None # if unset, defaults to self.name+"> "
+ version = None # if set, default top-level options include --version
+
+ # Default messages for some 'help' command error cases.
+ # They are interpolated with one arg: the command.
+ nohelp = "no help on '%s'"
+ unknowncmd = "unknown command: '%s'"
+
+ helpindent = '' # string with which to indent help output
+
+ # Default man page parts, please change them in subclass
+ man_header = MAN_HEADER
+ man_commands_header = MAN_COMMANDS_HEADER
+ man_options_header = MAN_OPTIONS_HEADER
+ man_footer = MAN_FOOTER
+
+ def __init__(self, completekey='tab',
+ stdin=None, stdout=None, stderr=None):
+ """Cmdln(completekey='tab', stdin=None, stdout=None, stderr=None)
+
+ The optional argument 'completekey' is the readline name of a
+ completion key; it defaults to the Tab key. If completekey is
+ not None and the readline module is available, command completion
+ is done automatically.
+
+ The optional arguments 'stdin', 'stdout' and 'stderr' specify
+ alternate input, output and error output file objects; if not
+ specified, sys.* are used.
+
+ If 'stdout' but not 'stderr' is specified, stdout is used for
+ error output. This is to provide least surprise for users used
+ to only the 'stdin' and 'stdout' options with cmd.Cmd.
+ """
+ if self.name is None:
+ self.name = os.path.basename(sys.argv[0])
+ if self.prompt is None:
+ self.prompt = self.name+"> "
+ self._name_str = self._str(self.name)
+ self._prompt_str = self._str(self.prompt)
+ if stdin is not None:
+ self.stdin = stdin
+ else:
+ self.stdin = sys.stdin
+ if stdout is not None:
+ self.stdout = stdout
+ else:
+ self.stdout = sys.stdout
+ if stderr is not None:
+ self.stderr = stderr
+ elif stdout is not None:
+ self.stderr = stdout
+ else:
+ self.stderr = sys.stderr
+ self.cmdqueue = []
+ self.completekey = completekey
+ self.cmdlooping = False
+
+ def get_optparser(self):
+ """Hook for subclasses to set the option parser for the
+ top-level command/shell.
+
+ This option parser is used retrieved and used by `.main()' to
+ handle top-level options.
+
+ The default implements a single '-h|--help' option. Sub-classes
+ can return None to have no options at the top-level. Typically
+ an instance of CmdlnOptionParser should be returned.
+ """
+ version = (self.version is not None
+ and "%s %s" % (self._name_str, self.version)
+ or None)
+ return CmdlnOptionParser(self, version=version)
+
+ def get_version(self):
+ """
+ Returns version of program. To be replaced in subclass.
+ """
+ return __version__
+
+ def postoptparse(self):
+ """Hook method executed just after `.main()' parses top-level
+ options.
+
+ When called `self.values' holds the results of the option parse.
+ """
+ pass
+
+ def main(self, argv=None, loop=LOOP_NEVER):
+ """A possible mainline handler for a script, like so:
+
+ import cmdln
+ class MyCmd(cmdln.Cmdln):
+ name = "mycmd"
+ ...
+
+ if __name__ == "__main__":
+ MyCmd().main()
+
+ By default this will use sys.argv to issue a single command to
+ 'MyCmd', then exit. The 'loop' argument can be use to control
+ interactive shell behaviour.
+
+ Arguments:
+ "argv" (optional, default sys.argv) is the command to run.
+ It must be a sequence, where the first element is the
+ command name and subsequent elements the args for that
+ command.
+ "loop" (optional, default LOOP_NEVER) is a constant
+ indicating if a command loop should be started (i.e. an
+ interactive shell). Valid values (constants on this module):
+ LOOP_ALWAYS start loop and run "argv", if any
+ LOOP_NEVER run "argv" (or .emptyline()) and exit
+ LOOP_IF_EMPTY run "argv", if given, and exit;
+ otherwise, start loop
+ """
+ if argv is None:
+ argv = sys.argv
+ else:
+ argv = argv[:] # don't modify caller's list
+
+ self.optparser = self.get_optparser()
+ if self.optparser: # i.e. optparser=None means don't process for opts
+ try:
+ self.options, args = self.optparser.parse_args(argv[1:])
+ except CmdlnUserError as ex:
+ msg = "%s: %s\nTry '%s help' for info.\n"\
+ % (self.name, ex, self.name)
+ self.stderr.write(self._str(msg))
+ self.stderr.flush()
+ return 1
+ except StopOptionProcessing as ex:
+ return 0
+ else:
+ self.options, args = None, argv[1:]
+ self.postoptparse()
+
+ if loop == LOOP_ALWAYS:
+ if args:
+ self.cmdqueue.append(args)
+ return self.cmdloop()
+ elif loop == LOOP_NEVER:
+ if args:
+ return self.cmd(args)
+ else:
+ return self.emptyline()
+ elif loop == LOOP_IF_EMPTY:
+ if args:
+ return self.cmd(args)
+ else:
+ return self.cmdloop()
+
+ def cmd(self, argv):
+ """Run one command and exit.
+
+ "argv" is the arglist for the command to run. argv[0] is the
+ command to run. If argv is an empty list then the
+ 'emptyline' handler is run.
+
+ Returns the return value from the command handler.
+ """
+ assert isinstance(argv, (list, tuple)), \
+ "'argv' is not a sequence: %r" % argv
+ retval = None
+ try:
+ argv = self.precmd(argv)
+ retval = self.onecmd(argv)
+ self.postcmd(argv)
+ except:
+ if not self.cmdexc(argv):
+ raise
+ retval = 1
+ return retval
+
+ def _str(self, s):
+ """Safely convert the given str/unicode to a string for printing."""
+ try:
+ return str(s)
+ except UnicodeError:
+ #XXX What is the proper encoding to use here? 'utf-8' seems
+ # to work better than "getdefaultencoding" (usually
+ # 'ascii'), on OS X at least.
+ #return s.encode(sys.getdefaultencoding(), "replace")
+ return s.encode("utf-8", "replace")
+
+ def cmdloop(self, intro=None):
+ """Repeatedly issue a prompt, accept input, parse into an argv, and
+ dispatch (via .precmd(), .onecmd() and .postcmd()), passing them
+ the argv. In other words, start a shell.
+
+ "intro" (optional) is a introductory message to print when
+ starting the command loop. This overrides the class
+ "intro" attribute, if any.
+ """
+ self.cmdlooping = True
+ self.preloop()
+ if self.use_rawinput and self.completekey:
+ try:
+ import readline
+ self.old_completer = readline.get_completer()
+ readline.set_completer(self.complete)
+ readline.parse_and_bind(self.completekey+": complete")
+ except ImportError:
+ pass
+ try:
+ if intro is None:
+ intro = self.intro
+ if intro:
+ intro_str = self._str(intro)
+ self.stdout.write(intro_str+'\n')
+ self.stop = False
+ retval = None
+ while not self.stop:
+ if self.cmdqueue:
+ argv = self.cmdqueue.pop(0)
+ assert isinstance(argv, (list, tuple)), \
+ "item on 'cmdqueue' is not a sequence: %r" % argv
+ else:
+ if self.use_rawinput:
+ try:
+ try:
+ #python 2.x
+ line = raw_input(self._prompt_str)
+ except NameError:
+ line = input(self._prompt_str)
+ except EOFError:
+ line = 'EOF'
+ else:
+ self.stdout.write(self._prompt_str)
+ self.stdout.flush()
+ line = self.stdin.readline()
+ if not len(line):
+ line = 'EOF'
+ else:
+ line = line[:-1] # chop '\n'
+ argv = line2argv(line)
+ try:
+ argv = self.precmd(argv)
+ retval = self.onecmd(argv)
+ self.postcmd(argv)
+ except:
+ if not self.cmdexc(argv):
+ raise
+ retval = 1
+ self.lastretval = retval
+ self.postloop()
+ finally:
+ if self.use_rawinput and self.completekey:
+ try:
+ import readline
+ readline.set_completer(self.old_completer)
+ except ImportError:
+ pass
+ self.cmdlooping = False
+ return retval
+
+ def precmd(self, argv):
+ """Hook method executed just before the command argv is
+ interpreted, but after the input prompt is generated and issued.
+
+ "argv" is the cmd to run.
+
+ Returns an argv to run (i.e. this method can modify the command
+ to run).
+ """
+ return argv
+
+ def postcmd(self, argv):
+ """Hook method executed just after a command dispatch is finished.
+
+ "argv" is the command that was run.
+ """
+ pass
+
+ def cmdexc(self, argv):
+ """Called if an exception is raised in any of precmd(), onecmd(),
+ or postcmd(). If True is returned, the exception is deemed to have
+ been dealt with. Otherwise, the exception is re-raised.
+
+ The default implementation handles CmdlnUserError's, which
+ typically correspond to user error in calling commands (as
+ opposed to programmer error in the design of the script using
+ cmdln.py).
+ """
+ exc_type, exc, traceback = sys.exc_info()
+ if isinstance(exc, CmdlnUserError):
+ msg = "%s %s: %s\nTry '%s help %s' for info.\n"\
+ % (self.name, argv[0], exc, self.name, argv[0])
+ self.stderr.write(self._str(msg))
+ self.stderr.flush()
+ return True
+
+ def onecmd(self, argv):
+ if not argv:
+ return self.emptyline()
+ self.lastcmd = argv
+ cmdname = self._get_canonical_cmd_name(argv[0])
+ if cmdname:
+ handler = self._get_cmd_handler(cmdname)
+ if handler:
+ return self._dispatch_cmd(handler, argv)
+ return self.default(argv)
+
+ def _dispatch_cmd(self, handler, argv):
+ return handler(argv)
+
+ def default(self, argv):
+ """Hook called to handle a command for which there is no handler.
+
+ "argv" is the command and arguments to run.
+
+ The default implementation writes and error message to stderr
+ and returns an error exit status.
+
+ Returns a numeric command exit status.
+ """
+ errmsg = self._str(self.unknowncmd % (argv[0],))
+ if self.cmdlooping:
+ self.stderr.write(errmsg+"\n")
+ else:
+ self.stderr.write("%s: %s\nTry '%s help' for info.\n"
+ % (self._name_str, errmsg, self._name_str))
+ self.stderr.flush()
+ return 1
+
+ def parseline(self, line):
+ # This is used by Cmd.complete (readline completer function) to
+ # massage the current line buffer before completion processing.
+ # We override to drop special '!' handling.
+ line = line.strip()
+ if not line:
+ return None, None, line
+ elif line[0] == '?':
+ line = 'help ' + line[1:]
+ i, n = 0, len(line)
+ while i < n and line[i] in self.identchars:
+ i = i+1
+ cmd, arg = line[:i], line[i:].strip()
+ return cmd, arg, line
+
+ def helpdefault(self, cmd, known):
+ """Hook called to handle help on a command for which there is no
+ help handler.
+
+ "cmd" is the command name on which help was requested.
+ "known" is a boolean indicating if this command is known
+ (i.e. if there is a handler for it).
+
+ Returns a return code.
+ """
+ if known:
+ msg = self._str(self.nohelp % (cmd,))
+ if self.cmdlooping:
+ self.stderr.write(msg + '\n')
+ else:
+ self.stderr.write("%s: %s\n" % (self.name, msg))
+ else:
+ msg = self.unknowncmd % (cmd,)
+ if self.cmdlooping:
+ self.stderr.write(msg + '\n')
+ else:
+ self.stderr.write("%s: %s\n"
+ "Try '%s help' for info.\n"
+ % (self.name, msg, self.name))
+ self.stderr.flush()
+ return 1
+
+
+ def do_help(self, argv):
+ """${cmd_name}: give detailed help on a specific sub-command
+
+ usage:
+ ${name} help [SUBCOMMAND]
+ """
+ if len(argv) > 1: # asking for help on a particular command
+ doc = None
+ cmdname = self._get_canonical_cmd_name(argv[1]) or argv[1]
+ if not cmdname:
+ return self.helpdefault(argv[1], False)
+ else:
+ helpfunc = getattr(self, "help_"+cmdname, None)
+ if helpfunc:
+ doc = helpfunc()
+ else:
+ handler = self._get_cmd_handler(cmdname)
+ if handler:
+ doc = handler.__doc__
+ if doc is None:
+ return self.helpdefault(argv[1], handler != None)
+ else: # bare "help" command
+ doc = self.__class__.__doc__ # try class docstring
+ if doc is None:
+ # Try to provide some reasonable useful default help.
+ if self.cmdlooping:
+ prefix = ""
+ else:
+ prefix = self.name+' '
+ doc = """usage:
+ %sSUBCOMMAND [ARGS...]
+ %shelp [SUBCOMMAND]
+
+ ${option_list}
+ ${command_list}
+ ${help_list}
+ """ % (prefix, prefix)
+ cmdname = None
+
+ if doc: # *do* have help content, massage and print that
+ doc = self._help_reindent(doc)
+ doc = self._help_preprocess(doc, cmdname)
+ doc = doc.rstrip() + '\n' # trim down trailing space
+ self.stdout.write(self._str(doc))
+ self.stdout.flush()
+ do_help.aliases = ["?"]
+
+
+ def do_man(self, argv):
+ """${cmd_name}: generates a man page
+
+ usage:
+ ${name} man
+ """
+ self.stdout.write(bytes(
+ self.man_header % {
+ 'date': date.today().strftime('%b %Y'),
+ 'version': self.get_version(),
+ 'name': self.name,
+ 'ucname': self.name.upper()
+ },
+ "utf-8"))
+
+ self.stdout.write(bytes(self.man_commands_header, "utf-8"))
+ commands = self._help_get_command_list()
+ for command, doc in commands:
+ cmdname = command.split(' ')[0]
+ text = self._help_preprocess(doc, cmdname)
+ lines = []
+ for line in text.splitlines(False):
+ if line[:8] == ' ' * 8:
+ line = line[8:]
+ lines.append(man_escape(line))
+
+ self.stdout.write(bytes(
+ '.TP\n\\fB%s\\fR\n%s\n' % (command, '\n'.join(lines)), "utf-8"))
+
+ self.stdout.write(bytes(self.man_options_header, "utf-8"))
+ self.stdout.write(bytes(
+ man_escape(self._help_preprocess('${option_list}', None)), "utf-8"))
+
+ self.stdout.write(bytes(self.man_footer, "utf-8"))
+
+ self.stdout.flush()
+
+ def _help_reindent(self, help, indent=None):
+ """Hook to re-indent help strings before writing to stdout.
+
+ "help" is the help content to re-indent
+ "indent" is a string with which to indent each line of the
+ help content after normalizing. If unspecified or None
+ then the default is use: the 'self.helpindent' class
+ attribute. By default this is the empty string, i.e.
+ no indentation.
+
+ By default, all common leading whitespace is removed and then
+ the lot is indented by 'self.helpindent'. When calculating the
+ common leading whitespace the first line is ignored -- hence
+ help content for Conan can be written as follows and have the
+ expected indentation:
+
+ def do_crush(self, ...):
+ '''${cmd_name}: crush your enemies, see them driven before you...
+
+ c.f. Conan the Barbarian'''
+ """
+ if indent is None:
+ indent = self.helpindent
+ lines = help.splitlines(0)
+ _dedentlines(lines, skip_first_line=True)
+ lines = [(indent+line).rstrip() for line in lines]
+ return '\n'.join(lines)
+
+ def _help_preprocess(self, help, cmdname):
+ """Hook to preprocess a help string before writing to stdout.
+
+ "help" is the help string to process.
+ "cmdname" is the canonical sub-command name for which help
+ is being given, or None if the help is not specific to a
+ command.
+
+ By default the following template variables are interpolated in
+ help content. (Note: these are similar to Python 2.4's
+ string.Template interpolation but not quite.)
+
+ ${name}
+ The tool's/shell's name, i.e. 'self.name'.
+ ${option_list}
+ A formatted table of options for this shell/tool.
+ ${command_list}
+ A formatted table of available sub-commands.
+ ${help_list}
+ A formatted table of additional help topics (i.e. 'help_*'
+ methods with no matching 'do_*' method).
+ ${cmd_name}
+ The name (and aliases) for this sub-command formatted as:
+ "NAME (ALIAS1, ALIAS2, ...)".
+ ${cmd_usage}
+ A formatted usage block inferred from the command function
+ signature.
+ ${cmd_option_list}
+ A formatted table of options for this sub-command. (This is
+ only available for commands using the optparse integration,
+ i.e. using @cmdln.option decorators or manually setting the
+ 'optparser' attribute on the 'do_*' method.)
+
+ Returns the processed help.
+ """
+ preprocessors = {
+ "${name}": self._help_preprocess_name,
+ "${option_list}": self._help_preprocess_option_list,
+ "${command_list}": self._help_preprocess_command_list,
+ "${help_list}": self._help_preprocess_help_list,
+ "${cmd_name}": self._help_preprocess_cmd_name,
+ "${cmd_usage}": self._help_preprocess_cmd_usage,
+ "${cmd_option_list}": self._help_preprocess_cmd_option_list,
+ }
+
+ for marker, preprocessor in preprocessors.items():
+ if marker in help:
+ help = preprocessor(help, cmdname)
+ return help
+
+ def _help_preprocess_name(self, help, cmdname=None):
+ return help.replace("${name}", self.name)
+
+ def _help_preprocess_option_list(self, help, cmdname=None):
+ marker = "${option_list}"
+ indent, indent_width = _get_indent(marker, help)
+ suffix = _get_trailing_whitespace(marker, help)
+
+ if self.optparser:
+ # Setup formatting options and format.
+ # - Indentation of 4 is better than optparse default of 2.
+ # C.f. Damian Conway's discussion of this in Perl Best
+ # Practices.
+ self.optparser.formatter.indent_increment = 4
+ self.optparser.formatter.current_indent = indent_width
+ block = self.optparser.format_option_help() + '\n'
+ else:
+ block = ""
+
+ help_msg = help.replace(indent+marker+suffix, block, 1)
+ return help_msg
+
+ def _help_get_command_list(self):
+ # Find any aliases for commands.
+ token2canonical = self._get_canonical_map()
+ aliases = {}
+ for token, cmdname in token2canonical.items():
+ if token == cmdname:
+ continue
+ aliases.setdefault(cmdname, []).append(token)
+
+ # Get the list of (non-hidden) commands and their
+ # documentation, if any.
+ cmdnames = {} # use a dict to strip duplicates
+ for attr in self.get_names():
+ if attr.startswith("do_"):
+ cmdnames[attr[3:]] = True
+ linedata = []
+ for cmdname in sorted(cmdnames.keys()):
+ if aliases.get(cmdname):
+ a = sorted(aliases[cmdname])
+ cmdstr = "%s (%s)" % (cmdname, ", ".join(a))
+ else:
+ cmdstr = cmdname
+ doc = None
+ try:
+ helpfunc = getattr(self, 'help_'+cmdname)
+ except AttributeError:
+ handler = self._get_cmd_handler(cmdname)
+ if handler:
+ doc = handler.__doc__
+ else:
+ doc = helpfunc()
+
+ # Strip "${cmd_name}: " from the start of a command's doc. Best
+ # practice dictates that command help strings begin with this, but
+ # it isn't at all wanted for the command list.
+ to_strip = "${cmd_name}:"
+ if doc and doc.startswith(to_strip):
+ #log.debug("stripping %r from start of %s's help string",
+ # to_strip, cmdname)
+ doc = doc[len(to_strip):].lstrip()
+ if not getattr(self._get_cmd_handler(cmdname), "hidden", None):
+ linedata.append( (cmdstr, doc) )
+
+ return linedata
+
+ def _help_preprocess_command_list(self, help, cmdname=None):
+ marker = "${command_list}"
+ indent, indent_width = _get_indent(marker, help)
+ suffix = _get_trailing_whitespace(marker, help)
+
+ linedata = self._help_get_command_list()
+
+ if linedata:
+ subindent = indent + ' '*4
+ lines = _format_linedata(linedata, subindent, indent_width+4)
+ block = indent + "commands:\n" \
+ + '\n'.join(lines) + "\n\n"
+ help = help.replace(indent+marker+suffix, block, 1)
+ return help
+
+ def _help_preprocess_help_list(self, help, cmdname=None):
+ marker = "${help_list}"
+ indent, indent_width = _get_indent(marker, help)
+ suffix = _get_trailing_whitespace(marker, help)
+
+ # Determine the additional help topics, if any.
+ helpnames = {}
+ token2cmdname = self._get_canonical_map()
+ for attr in self.get_names():
+ if not attr.startswith("help_"):
+ continue
+ helpname = attr[5:]
+ if helpname not in token2cmdname:
+ helpnames[helpname] = True
+
+ if helpnames:
+ helpnames = sorted(helpnames.keys())
+ linedata = [(self.name+" help "+n, "") for n in helpnames]
+
+ subindent = indent + ' '*4
+ lines = _format_linedata(linedata, subindent, indent_width+4)
+ block = indent + "additional help topics:\n" \
+ + '\n'.join(lines) + "\n\n"
+ else:
+ block = ''
+ help_msg = help.replace(indent+marker+suffix, block, 1)
+ return help_msg
+
+ def _help_preprocess_cmd_name(self, help, cmdname=None):
+ marker = "${cmd_name}"
+ handler = self._get_cmd_handler(cmdname)
+ if not handler:
+ raise CmdlnError("cannot preprocess '%s' into help string: "
+ "could not find command handler for %r"
+ % (marker, cmdname))
+ s = cmdname
+ if hasattr(handler, "aliases"):
+ s += " (%s)" % (", ".join(handler.aliases))
+ help_msg = help.replace(marker, s)
+ return help_msg
+
+ #TODO: this only makes sense as part of the Cmdln class.
+ # Add hooks to add help preprocessing template vars and put
+ # this one on that class.
+ def _help_preprocess_cmd_usage(self, help, cmdname=None):
+ marker = "${cmd_usage}"
+ handler = self._get_cmd_handler(cmdname)
+ if not handler:
+ raise CmdlnError("cannot preprocess '%s' into help string: "
+ "could not find command handler for %r"
+ % (marker, cmdname))
+ indent, indent_width = _get_indent(marker, help)
+ suffix = _get_trailing_whitespace(marker, help)
+
+ func_defaults, co_argcount, co_varnames, co_flags, _ = introspect_handler(handler)
+ CO_FLAGS_ARGS = 4
+ CO_FLAGS_KWARGS = 8
+
+ # Adjust argcount for possible *args and **kwargs arguments.
+ argcount = co_argcount
+ if co_flags & CO_FLAGS_ARGS:
+ argcount += 1
+ if co_flags & CO_FLAGS_KWARGS:
+ argcount += 1
+
+ # Determine the usage string.
+ usage = "%s %s" % (self.name, cmdname)
+ if argcount <= 2: # handler ::= do_FOO(self, argv)
+ usage += " [ARGS...]"
+ elif argcount >= 3: # handler ::= do_FOO(self, subcmd, opts, ...)
+ argnames = list(co_varnames[3:argcount])
+ tail = ""
+ if co_flags & CO_FLAGS_KWARGS:
+ name = argnames.pop(-1)
+ import warnings
+ # There is no generally accepted mechanism for passing
+ # keyword arguments from the command line. Could
+ # *perhaps* consider: arg=value arg2=value2 ...
+ warnings.warn("argument '**%s' on '%s.%s' command "
+ "handler will never get values"
+ % (name, self.__class__.__name__,
+ getattr(func, "__name__", getattr(func, "func_name"))))
+ if co_flags & CO_FLAGS_ARGS:
+ name = argnames.pop(-1)
+ tail = "[%s...]" % name.upper()
+ while func_defaults:
+ func_defaults.pop(-1)
+ name = argnames.pop(-1)
+ tail = "[%s%s%s]" % (name.upper(), (tail and ' ' or ''), tail)
+ while argnames:
+ name = argnames.pop(-1)
+ tail = "%s %s" % (name.upper(), tail)
+ usage += ' ' + tail
+
+ block_lines = [
+ self.helpindent + "Usage:",
+ self.helpindent + ' '*4 + usage
+ ]
+ block = '\n'.join(block_lines) + '\n\n'
+
+ help_msg = help.replace(indent+marker+suffix, block, 1)
+ return help_msg
+
+ #TODO: this only makes sense as part of the Cmdln class.
+ # Add hooks to add help preprocessing template vars and put
+ # this one on that class.
+ def _help_preprocess_cmd_option_list(self, help, cmdname=None):
+ marker = "${cmd_option_list}"
+ handler = self._get_cmd_handler(cmdname)
+ if not handler:
+ raise CmdlnError("cannot preprocess '%s' into help string: "
+ "could not find command handler for %r"
+ % (marker, cmdname))
+ indent, indent_width = _get_indent(marker, help)
+ suffix = _get_trailing_whitespace(marker, help)
+ if hasattr(handler, "optparser"):
+ # Setup formatting options and format.
+ # - Indentation of 4 is better than optparse default of 2.
+ # C.f. Damian Conway's discussion of this in Perl Best
+ # Practices.
+ handler.optparser.formatter.indent_increment = 4
+ handler.optparser.formatter.current_indent = indent_width
+ block = handler.optparser.format_option_help() + '\n'
+ else:
+ block = ""
+
+ help_msg = help.replace(indent+marker+suffix, block, 1)
+ return help_msg
+
+ def _get_canonical_cmd_name(self, token):
+ c_map = self._get_canonical_map()
+ return c_map.get(token, None)
+
+ def _get_canonical_map(self):
+ """Return a mapping of available command names and aliases to
+ their canonical command name.
+ """
+ cacheattr = "_token2canonical"
+ if not hasattr(self, cacheattr):
+ # Get the list of commands and their aliases, if any.
+ token2canonical = {}
+ cmd2funcname = {} # use a dict to strip duplicates
+ for attr in self.get_names():
+ if attr.startswith("do_"):
+ cmdname = attr[3:]
+ elif attr.startswith("_do_"):
+ cmdname = attr[4:]
+ else:
+ continue
+ cmd2funcname[cmdname] = attr
+ token2canonical[cmdname] = cmdname
+ for cmdname, funcname in cmd2funcname.items(): # add aliases
+ func = getattr(self, funcname)
+ aliases = getattr(func, "aliases", [])
+ for alias in aliases:
+ if alias in cmd2funcname:
+ import warnings
+ warnings.warn("'%s' alias for '%s' command conflicts "
+ "with '%s' handler"
+ % (alias, cmdname, cmd2funcname[alias]))
+ continue
+ token2canonical[alias] = cmdname
+ setattr(self, cacheattr, token2canonical)
+ return getattr(self, cacheattr)
+
+ def _get_cmd_handler(self, cmdname):
+ handler = None
+ try:
+ handler = getattr(self, 'do_' + cmdname)
+ except AttributeError:
+ try:
+ # Private command handlers begin with "_do_".
+ handler = getattr(self, '_do_' + cmdname)
+ except AttributeError:
+ pass
+ return handler
+
+ def _do_EOF(self, argv):
+ # Default EOF handler
+ # Note: an actual EOF is redirected to this command.
+ #TODO: separate name for this. Currently it is available from
+ # command-line. Is that okay?
+ self.stdout.write('\n')
+ self.stdout.flush()
+ self.stop = True
+
+ def emptyline(self):
+ # Different from cmd.Cmd: don't repeat the last command for an
+ # emptyline.
+ if self.cmdlooping:
+ pass
+ else:
+ return self.do_help(["help"])
+
+
+#---- optparse.py extension to fix (IMO) some deficiencies
+#
+# See the class _OptionParserEx docstring for details.
+#
+
+class StopOptionProcessing(Exception):
+ """Indicate that option *and argument* processing should stop
+ cleanly. This is not an error condition. It is similar in spirit to
+ StopIteration. This is raised by _OptionParserEx's default "help"
+ and "version" option actions and can be raised by custom option
+ callbacks too.
+
+ Hence the typical CmdlnOptionParser (a subclass of _OptionParserEx)
+ usage is:
+
+ parser = CmdlnOptionParser(mycmd)
+ parser.add_option("-f", "--force", dest="force")
+ ...
+ try:
+ opts, args = parser.parse_args()
+ except StopOptionProcessing:
+ # normal termination, "--help" was probably given
+ sys.exit(0)
+ """
+
+class _OptionParserEx(optparse.OptionParser):
+ """An optparse.OptionParser that uses exceptions instead of sys.exit.
+
+ This class is an extension of optparse.OptionParser that differs
+ as follows:
+ - Correct (IMO) the default OptionParser error handling to never
+ sys.exit(). Instead OptParseError exceptions are passed through.
+ - Add the StopOptionProcessing exception (a la StopIteration) to
+ indicate normal termination of option processing.
+ See StopOptionProcessing's docstring for details.
+
+ I'd also like to see the following in the core optparse.py, perhaps
+ as a RawOptionParser which would serve as a base class for the more
+ generally used OptionParser (that works as current):
+ - Remove the implicit addition of the -h|--help and --version
+ options. They can get in the way (e.g. if want '-?' and '-V' for
+ these as well) and it is not hard to do:
+ optparser.add_option("-h", "--help", action="help")
+ optparser.add_option("--version", action="version")
+ These are good practices, just not valid defaults if they can
+ get in the way.
+ """
+ def error(self, msg):
+ raise optparse.OptParseError(msg)
+
+ def exit(self, status=0, msg=None):
+ if status == 0:
+ raise StopOptionProcessing(msg)
+ else:
+ #TODO: don't lose status info here
+ raise optparse.OptParseError(msg)
+
+
+
+#---- optparse.py-based option processing support
+
+class CmdlnOptionParser(_OptionParserEx):
+ """An optparse.OptionParser class more appropriate for top-level
+ Cmdln options. For parsing of sub-command options, see
+ SubCmdOptionParser.
+
+ Changes:
+ - disable_interspersed_args() by default, because a Cmdln instance
+ has sub-commands which may themselves have options.
+ - Redirect print_help() to the Cmdln.do_help() which is better
+ equiped to handle the "help" action.
+ - error() will raise a CmdlnUserError: OptionParse.error() is meant
+ to be called for user errors. Raising a well-known error here can
+ make error handling clearer.
+ - Also see the changes in _OptionParserEx.
+ """
+ def __init__(self, cmdln, **kwargs):
+ self.cmdln = cmdln
+ kwargs["prog"] = self.cmdln.name
+ _OptionParserEx.__init__(self, **kwargs)
+ self.disable_interspersed_args()
+
+ def print_help(self, file=None):
+ self.cmdln.onecmd(["help"])
+
+ def error(self, msg):
+ raise CmdlnUserError(msg)
+
+
+class SubCmdOptionParser(_OptionParserEx):
+ def set_cmdln_info(self, cmdln, subcmd):
+ """Called by Cmdln to pass relevant info about itself needed
+ for print_help().
+ """
+ self.cmdln = cmdln
+ self.subcmd = subcmd
+
+ def print_help(self, file=None):
+ self.cmdln.onecmd(["help", self.subcmd])
+
+ def error(self, msg):
+ raise CmdlnUserError(msg)
+
+
+def option(*args, **kwargs):
+ """Decorator to add an option to the optparser argument of a Cmdln
+ subcommand.
+
+ Example:
+ class MyShell(cmdln.Cmdln):
+ @cmdln.option("-f", "--force", help="force removal")
+ def do_remove(self, subcmd, opts, *args):
+ #...
+ """
+ #XXX Is there a possible optimization for many options to not have a
+ # large stack depth here?
+ def decorate(f):
+ if not hasattr(f, "optparser"):
+ f.optparser = SubCmdOptionParser()
+ f.optparser.add_option(*args, **kwargs)
+ return f
+ return decorate
+
+def hide(*args):
+ """For obsolete calls, hide them in help listings.
+
+ Example:
+ class MyShell(cmdln.Cmdln):
+ @cmdln.hide()
+ def do_shell(self, argv):
+ #...implement 'shell' command
+ """
+ def decorate(f):
+ f.hidden = 1
+ return f
+ return decorate
+
+
+class Cmdln(RawCmdln):
+ """An improved (on cmd.Cmd) framework for building multi-subcommand
+ scripts (think "svn" & "cvs") and simple shells (think "pdb" and
+ "gdb").
+
+ A simple example:
+
+ import cmdln
+
+ class MySVN(cmdln.Cmdln):
+ name = "svn"
+
+ @cmdln.aliases('stat', 'st')
+ @cmdln.option('-v', '--verbose', action='store_true'
+ help='print verbose information')
+ def do_status(self, subcmd, opts, *paths):
+ print "handle 'svn status' command"
+
+ #...
+
+ if __name__ == "__main__":
+ shell = MySVN()
+ retval = shell.main()
+ sys.exit(retval)
+
+ 'Cmdln' extends 'RawCmdln' by providing optparse option processing
+ integration. See this class' _dispatch_cmd() docstring and
+ <http://trentm.com/projects/cmdln> for more information.
+ """
+ def _dispatch_cmd(self, handler, argv):
+ """Introspect sub-command handler signature to determine how to
+ dispatch the command. The raw handler provided by the base
+ 'RawCmdln' class is still supported:
+
+ def do_foo(self, argv):
+ # 'argv' is the vector of command line args, argv[0] is
+ # the command name itself (i.e. "foo" or an alias)
+ pass
+
+ In addition, if the handler has more than 2 arguments option
+ processing is automatically done (using optparse):
+
+ @cmdln.option('-v', '--verbose', action='store_true')
+ def do_bar(self, subcmd, opts, *args):
+ # subcmd = <"bar" or an alias>
+ # opts = <an optparse.Values instance>
+ if opts.verbose:
+ print "lots of debugging output..."
+ # args = <tuple of arguments>
+ for arg in args:
+ bar(arg)
+
+ TODO: explain that "*args" can be other signatures as well.
+
+ The `cmdln.option` decorator corresponds to an `add_option()`
+ method call on an `optparse.OptionParser` instance.
+
+ You can declare a specific number of arguments:
+
+ @cmdln.option('-v', '--verbose', action='store_true')
+ def do_bar2(self, subcmd, opts, bar_one, bar_two):
+ #...
+
+ and an appropriate error message will be raised/printed if the
+ command is called with a different number of args.
+ """
+ co_argcount = introspect_handler(handler)[1]
+ if co_argcount == 2: # handler ::= do_foo(self, argv)
+ return handler(argv)
+ elif co_argcount >= 3: # handler ::= do_foo(self, subcmd, opts, ...)
+ try:
+ optparser = handler.optparser
+ except AttributeError:
+ optparser = introspect_handler(handler)[4].optparser = SubCmdOptionParser()
+ assert isinstance(optparser, SubCmdOptionParser)
+ optparser.set_cmdln_info(self, argv[0])
+ try:
+ opts, args = optparser.parse_args(argv[1:])
+ except StopOptionProcessing:
+ #TODO: this doesn't really fly for a replacement of
+ # optparse.py behaviour, does it?
+ return 0 # Normal command termination
+
+ try:
+ return handler(argv[0], opts, *args)
+ except TypeError as ex:
+ # Some TypeError's are user errors:
+ # do_foo() takes at least 4 arguments (3 given)
+ # do_foo() takes at most 5 arguments (6 given)
+ # do_foo() takes exactly 5 arguments (6 given)
+ # Raise CmdlnUserError for these with a suitably
+ # massaged error message.
+ tb = sys.exc_info()[2] # the traceback object
+ if tb.tb_next is not None:
+ # If the traceback is more than one level deep, then the
+ # TypeError do *not* happen on the "handler(...)" call
+ # above. In that we don't want to handle it specially
+ # here: it would falsely mask deeper code errors.
+ raise
+ msg = ex.args[0]
+ match = _INCORRECT_NUM_ARGS_RE.search(msg)
+ if match:
+ msg = list(match.groups())
+ msg[1] = int(msg[1]) - 3
+ if msg[1] == 1:
+ msg[2] = msg[2].replace("arguments", "argument")
+ msg[3] = int(msg[3]) - 3
+ msg = ''.join(map(str, msg))
+ raise CmdlnUserError(msg)
+ else:
+ raise
+ else:
+ raise CmdlnError("incorrect argcount for %s(): takes %d, must "
+ "take 2 for 'argv' signature or 3+ for 'opts' "
+ "signature" % (handler.__name__, co_argcount))
+
+
+
+#---- internal support functions
+
+def _format_linedata(linedata, indent, indent_width):
+ """Format specific linedata into a pleasant layout.
+
+ "linedata" is a list of 2-tuples of the form:
+ (<item-display-string>, <item-docstring>)
+ "indent" is a string to use for one level of indentation
+ "indent_width" is a number of columns by which the
+ formatted data will be indented when printed.
+
+ The <item-display-string> column is held to 15 columns.
+ """
+ lines = []
+ WIDTH = 78 - indent_width
+ SPACING = 3
+ MAX_NAME_WIDTH = 15
+
+ NAME_WIDTH = min(max([len(s) for s, d in linedata]), MAX_NAME_WIDTH)
+ DOC_WIDTH = WIDTH - NAME_WIDTH - SPACING
+ for namestr, doc in linedata:
+ line = indent + namestr
+ if len(namestr) <= NAME_WIDTH:
+ line += ' ' * (NAME_WIDTH + SPACING - len(namestr))
+ else:
+ lines.append(line)
+ line = indent + ' ' * (NAME_WIDTH + SPACING)
+ line += _summarize_doc(doc, DOC_WIDTH)
+ lines.append(line.rstrip())
+ return lines
+
+def _summarize_doc(doc, length=60):
+ r"""Parse out a short one line summary from the given doclines.
+
+ "doc" is the doc string to summarize.
+ "length" is the max length for the summary
+
+ >>> _summarize_doc("this function does this")
+ 'this function does this'
+ >>> _summarize_doc("this function does this", 10)
+ 'this fu...'
+ >>> _summarize_doc("this function does this\nand that")
+ 'this function does this and that'
+ >>> _summarize_doc("this function does this\n\nand that")
+ 'this function does this'
+ """
+ if doc is None:
+ return ""
+ assert length > 3, "length <= 3 is absurdly short for a doc summary"
+ doclines = doc.strip().splitlines(0)
+ if not doclines:
+ return ""
+
+ summlines = []
+ for i, line in enumerate(doclines):
+ stripped = line.strip()
+ if not stripped:
+ break
+ summlines.append(stripped)
+ if len(''.join(summlines)) >= length:
+ break
+
+ summary = ' '.join(summlines)
+ if len(summary) > length:
+ summary = summary[:length-3] + "..."
+ return summary
+
+
+def line2argv(line):
+ r"""Parse the given line into an argument vector.
+
+ "line" is the line of input to parse.
+
+ This may get niggly when dealing with quoting and escaping. The
+ current state of this parsing may not be completely thorough/correct
+ in this respect.
+
+ >>> from cmdln import line2argv
+ >>> line2argv("foo")
+ ['foo']
+ >>> line2argv("foo bar")
+ ['foo', 'bar']
+ >>> line2argv("foo bar ")
+ ['foo', 'bar']
+ >>> line2argv(" foo bar")
+ ['foo', 'bar']
+
+ Quote handling:
+
+ >>> line2argv("'foo bar'")
+ ['foo bar']
+ >>> line2argv('"foo bar"')
+ ['foo bar']
+ >>> line2argv(r'"foo\"bar"')
+ ['foo"bar']
+ >>> line2argv("'foo bar' spam")
+ ['foo bar', 'spam']
+ >>> line2argv("'foo 'bar spam")
+ ['foo bar', 'spam']
+ >>> line2argv("'foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: command line is not terminated: unfinished single-quoted segment
+ >>> line2argv('"foo')
+ Traceback (most recent call last):
+ ...
+ ValueError: command line is not terminated: unfinished double-quoted segment
+ >>> line2argv('some\tsimple\ttests')
+ ['some', 'simple', 'tests']
+ >>> line2argv('a "more complex" test')
+ ['a', 'more complex', 'test']
+ >>> line2argv('a more="complex test of " quotes')
+ ['a', 'more=complex test of ', 'quotes']
+ >>> line2argv('a more" complex test of " quotes')
+ ['a', 'more complex test of ', 'quotes']
+ >>> line2argv('an "embedded \\"quote\\""')
+ ['an', 'embedded "quote"']
+ """
+ import string
+ line = line.strip()
+ argv = []
+ state = "default"
+ arg = None # the current argument being parsed
+ i = -1
+ while True:
+ i += 1
+ if i >= len(line):
+ break
+ ch = line[i]
+
+ if ch == "\\": # escaped char always added to arg, regardless of state
+ if arg is None:
+ arg = ""
+ i += 1
+ arg += line[i]
+ continue
+
+ if state == "single-quoted":
+ if ch == "'":
+ state = "default"
+ else:
+ arg += ch
+ elif state == "double-quoted":
+ if ch == '"':
+ state = "default"
+ else:
+ arg += ch
+ elif state == "default":
+ if ch == '"':
+ if arg is None:
+ arg = ""
+ state = "double-quoted"
+ elif ch == "'":
+ if arg is None:
+ arg = ""
+ state = "single-quoted"
+ elif ch in string.whitespace:
+ if arg is not None:
+ argv.append(arg)
+ arg = None
+ else:
+ if arg is None:
+ arg = ""
+ arg += ch
+ if arg is not None:
+ argv.append(arg)
+ if state != "default":
+ raise ValueError("command line is not terminated: unfinished %s "
+ "segment" % state)
+ return argv
+
+
+def argv2line(argv):
+ r"""Put together the given argument vector into a command line.
+
+ "argv" is the argument vector to process.
+
+ >>> from cmdln import argv2line
+ >>> argv2line(['foo'])
+ 'foo'
+ >>> argv2line(['foo', 'bar'])
+ 'foo bar'
+ >>> argv2line(['foo', 'bar baz'])
+ 'foo "bar baz"'
+ >>> argv2line(['foo"bar'])
+ 'foo"bar'
+ >>> print argv2line(['foo" bar'])
+ 'foo" bar'
+ >>> print argv2line(["foo' bar"])
+ "foo' bar"
+ >>> argv2line(["foo'bar"])
+ "foo'bar"
+ """
+ escapedArgs = []
+ for arg in argv:
+ if ' ' in arg and '"' not in arg:
+ arg = '"'+arg+'"'
+ elif ' ' in arg and "'" not in arg:
+ arg = "'"+arg+"'"
+ elif ' ' in arg:
+ arg = arg.replace('"', r'\"')
+ arg = '"'+arg+'"'
+ escapedArgs.append(arg)
+ return ' '.join(escapedArgs)
+
+
+# Recipe: dedent (0.1) in /Users/trentm/tm/recipes/cookbook
+def _dedentlines(lines, tabsize=8, skip_first_line=False):
+ """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
+
+ "lines" is a list of lines to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ Same as dedent() except operates on a sequence of lines. Note: the
+ lines list is modified **in-place**.
+ """
+ DEBUG = False
+ if DEBUG:
+ print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+ % (tabsize, skip_first_line))
+ indents = []
+ margin = None
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line:
+ continue
+ indent = 0
+ for ch in line:
+ if ch == ' ':
+ indent += 1
+ elif ch == '\t':
+ indent += tabsize - (indent % tabsize)
+ elif ch in '\r\n':
+ continue # skip all-whitespace lines
+ else:
+ break
+ else:
+ continue # skip all-whitespace lines
+ if DEBUG:
+ print("dedent: indent=%d: %r" % (indent, line))
+ if margin is None:
+ margin = indent
+ else:
+ margin = min(margin, indent)
+ if DEBUG:
+ print("dedent: margin=%r" % margin)
+
+ if margin is not None and margin > 0:
+ for i, line in enumerate(lines):
+ if i == 0 and skip_first_line:
+ continue
+ removed = 0
+ for j, ch in enumerate(line):
+ if ch == ' ':
+ removed += 1
+ elif ch == '\t':
+ removed += tabsize - (removed % tabsize)
+ elif ch in '\r\n':
+ if DEBUG:
+ print("dedent: %r: EOL -> strip up to EOL" % line)
+ lines[i] = lines[i][j:]
+ break
+ else:
+ raise ValueError("unexpected non-whitespace char %r in "
+ "line %r while removing %d-space margin"
+ % (ch, line, margin))
+ if DEBUG:
+ print("dedent: %r: %r -> removed %d/%d"\
+ % (line, ch, removed, margin))
+ if removed == margin:
+ lines[i] = lines[i][j+1:]
+ break
+ elif removed > margin:
+ lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+ break
+ return lines
+
+def _dedent(text, tabsize=8, skip_first_line=False):
+ """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
+
+ "text" is the text to dedent.
+ "tabsize" is the tab width to use for indent width calculations.
+ "skip_first_line" is a boolean indicating if the first line should
+ be skipped for calculating the indent width and for dedenting.
+ This is sometimes useful for docstrings and similar.
+
+ textwrap.dedent(s), but don't expand tabs to spaces
+ """
+ lines = text.splitlines(1)
+ _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
+ return ''.join(lines)
+
+
+def _get_indent(marker, s, tab_width=8):
+ """_get_indent(marker, s, tab_width=8) ->
+ (<indentation-of-'marker'>, <indentation-width>)"""
+ # Figure out how much the marker is indented.
+ INDENT_CHARS = tuple(' \t')
+ start = s.index(marker)
+ i = start
+ while i > 0:
+ if s[i-1] not in INDENT_CHARS:
+ break
+ i -= 1
+ indent = s[i:start]
+ indent_width = 0
+ for ch in indent:
+ if ch == ' ':
+ indent_width += 1
+ elif ch == '\t':
+ indent_width += tab_width - (indent_width % tab_width)
+ return indent, indent_width
+
+def _get_trailing_whitespace(marker, s):
+ """Return the whitespace content trailing the given 'marker' in string 's',
+ up to and including a newline.
+ """
+ suffix = ''
+ start = s.index(marker) + len(marker)
+ i = start
+ while i < len(s):
+ if s[i] in ' \t':
+ suffix += s[i]
+ elif s[i] in '\r\n':
+ suffix += s[i]
+ if s[i] == '\r' and i+1 < len(s) and s[i+1] == '\n':
+ suffix += s[i+1]
+ break
+ else:
+ break
+ i += 1
+ return suffix
+
+
+# vim: sw=4 et
diff --git a/osc/commandline.py b/osc/commandline.py
new file mode 100644
index 0000000..7a05e7f
--- /dev/null
+++ b/osc/commandline.py
@@ -0,0 +1,8496 @@
+# Copyright (C) 2006 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or version 3 (at your option).
+
+from __future__ import print_function
+
+from . import cmdln
+from . import conf
+from . import oscerr
+import sys
+import time
+import imp
+import inspect
+try:
+ from urllib.parse import urlsplit
+ from urllib.error import HTTPError
+ ET_ENCODING = "unicode"
+except ImportError:
+ #python 2.x
+ from urlparse import urlsplit
+ from urllib2 import HTTPError
+ ET_ENCODING = "utf-8"
+
+from optparse import SUPPRESS_HELP
+
+from .core import *
+from .util import safewriter
+
+MAN_HEADER = r""".TH %(ucname)s "1" "%(date)s" "%(name)s %(version)s" "User Commands"
+.SH NAME
+%(name)s \- openSUSE build service command-line tool.
+.SH SYNOPSIS
+.B %(name)s
+[\fIGLOBALOPTS\fR] \fISUBCOMMAND \fR[\fIOPTS\fR] [\fIARGS\fR...]
+.br
+.B %(name)s
+\fIhelp SUBCOMMAND\fR
+.SH DESCRIPTION
+openSUSE build service command-line tool.
+"""
+MAN_FOOTER = r"""
+.SH "SEE ALSO"
+Type 'osc help <subcommand>' for more detailed help on a specific subcommand.
+.PP
+For additional information, see
+ * http://en.opensuse.org/openSUSE:Build_Service_Tutorial
+ * http://en.opensuse.org/openSUSE:OSC
+.PP
+You can modify osc commands, or roll your own, via the plugin API:
+ * http://en.opensuse.org/openSUSE:OSC_plugins
+.SH AUTHOR
+osc was written by several authors. This man page is automatically generated.
+"""
+
+class Osc(cmdln.Cmdln):
+ """Usage: osc [GLOBALOPTS] SUBCOMMAND [OPTS] [ARGS...]
+ or: osc help SUBCOMMAND
+
+ openSUSE build service command-line tool.
+ Type 'osc help <subcommand>' for help on a specific subcommand.
+
+ ${command_list}
+ ${help_list}
+ global ${option_list}
+ For additional information, see
+ * http://en.opensuse.org/openSUSE:Build_Service_Tutorial
+ * http://en.opensuse.org/openSUSE:OSC
+
+ You can modify osc commands, or roll your own, via the plugin API:
+ * http://en.opensuse.org/openSUSE:OSC_plugins
+ """
+ name = 'osc'
+ conf = None
+
+ man_header = MAN_HEADER
+ man_footer = MAN_FOOTER
+
+ def __init__(self, *args, **kwargs):
+ # the plugins have to be loaded before the
+ # superclass' __init__ method is called
+ self._load_plugins()
+ cmdln.Cmdln.__init__(self, *args, **kwargs)
+ cmdln.Cmdln.do_help.aliases.append('h')
+ sys.stderr = safewriter.SafeWriter(sys.stderr)
+ sys.stdout = safewriter.SafeWriter(sys.stdout)
+
+ def get_version(self):
+ return get_osc_version()
+
+ def get_optparser(self):
+ """this is the parser for "global" options (not specific to subcommand)"""
+
+ optparser = cmdln.CmdlnOptionParser(self, version=get_osc_version())
+ optparser.add_option('--debugger', action='store_true',
+ help='jump into the debugger before executing anything')
+ optparser.add_option('--post-mortem', action='store_true',
+ help='jump into the debugger in case of errors')
+ optparser.add_option('-t', '--traceback', action='store_true',
+ help='print call trace in case of errors')
+ optparser.add_option('-H', '--http-debug', action='store_true',
+ help='debug HTTP traffic (filters some headers)')
+ optparser.add_option('--http-full-debug', action='store_true',
+ help='debug HTTP traffic (filters no headers)')
+ optparser.add_option('-d', '--debug', action='store_true',
+ help='print info useful for debugging')
+ optparser.add_option('-A', '--apiurl', dest='apiurl',
+ metavar='URL/alias',
+ help='specify URL to access API server at or an alias')
+ optparser.add_option('-c', '--config', dest='conffile',
+ metavar='FILE',
+ help='specify alternate configuration file')
+ optparser.add_option('--no-keyring', action='store_true',
+ help='disable usage of desktop keyring system')
+ optparser.add_option('--no-gnome-keyring', action='store_true',
+ help='disable usage of GNOME Keyring')
+ optparser.add_option('-v', '--verbose', dest='verbose', action='count', default=0,
+ help='increase verbosity')
+ optparser.add_option('-q', '--quiet', dest='verbose', action='store_const', const=-1,
+ help='be quiet, not verbose')
+ return optparser
+
+
+ def postoptparse(self, try_again = True):
+ """merge commandline options into the config"""
+ try:
+ conf.get_config(override_conffile = self.options.conffile,
+ override_apiurl = self.options.apiurl,
+ override_debug = self.options.debug,
+ override_http_debug = self.options.http_debug,
+ override_http_full_debug = self.options.http_full_debug,
+ override_traceback = self.options.traceback,
+ override_post_mortem = self.options.post_mortem,
+ override_no_keyring = self.options.no_keyring,
+ override_no_gnome_keyring = self.options.no_gnome_keyring,
+ override_verbose = self.options.verbose)
+ except oscerr.NoConfigfile as e:
+ print(e.msg, file=sys.stderr)
+ print('Creating osc configuration file %s ...' % e.file, file=sys.stderr)
+ import getpass
+ config = {}
+ config['user'] = raw_input('Username: ')
+ config['pass'] = getpass.getpass()
+ if self.options.no_keyring:
+ config['use_keyring'] = '0'
+ if self.options.no_gnome_keyring:
+ config['gnome_keyring'] = '0'
+ if self.options.apiurl:
+ config['apiurl'] = self.options.apiurl
+
+ conf.write_initial_config(e.file, config)
+ print('done', file=sys.stderr)
+ if try_again:
+ self.postoptparse(try_again = False)
+ except oscerr.ConfigMissingApiurl as e:
+ print(e.msg, file=sys.stderr)
+ import getpass
+ user = raw_input('Username: ')
+ passwd = getpass.getpass()
+ conf.add_section(e.file, e.url, user, passwd)
+ if try_again:
+ self.postoptparse(try_again = False)
+
+ self.options.verbose = conf.config['verbose']
+ self.download_progress = None
+ if conf.config.get('show_download_progress', False):
+ from .meter import TextMeter
+ self.download_progress = TextMeter(hide_finished=True)
+
+
+ def get_cmd_help(self, cmdname):
+ doc = self._get_cmd_handler(cmdname).__doc__
+ doc = self._help_reindent(doc)
+ doc = self._help_preprocess(doc, cmdname)
+ doc = doc.rstrip() + '\n' # trim down trailing space
+ return self._str(doc)
+
+ def get_api_url(self):
+ try:
+ localdir = os.getcwd()
+ except Exception as e:
+ ## check for Stale NFS file handle: '.'
+ try:
+ os.stat('.')
+ except Exception as ee:
+ e = ee
+ print("os.getcwd() failed: ", e, file=sys.stderr)
+ sys.exit(1)
+
+ if (is_package_dir(localdir) or is_project_dir(localdir)) and not self.options.apiurl:
+ return store_read_apiurl(os.curdir)
+ else:
+ return conf.config['apiurl']
+
+ # overridden from class Cmdln() to use config variables in help texts
+ def _help_preprocess(self, help, cmdname):
+ help_msg = cmdln.Cmdln._help_preprocess(self, help, cmdname)
+ return help_msg % conf.config
+
+
+ def do_init(self, subcmd, opts, project, package=None):
+ """${cmd_name}: Initialize a directory as working copy
+
+ Initialize an existing directory to be a working copy of an
+ (already existing) buildservice project/package.
+
+ (This is the same as checking out a package and then copying sources
+ into the directory. It does NOT create a new package. To create a
+ package, use 'osc meta pkg ... ...')
+
+ You wouldn't normally use this command.
+
+ To get a working copy of a package (e.g. for building it or working on
+ it, you would normally use the checkout command. Use "osc help
+ checkout" to get help for it.
+
+ usage:
+ osc init PRJ
+ osc init PRJ PAC
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+
+ if not package:
+ Project.init_project(apiurl, os.curdir, project, conf.config['do_package_tracking'])
+ print('Initializing %s (Project: %s)' % (os.curdir, project))
+ else:
+ Package.init_package(apiurl, project, package, os.curdir)
+ store_write_string(os.curdir, '_files', show_files_meta(apiurl, project, package) + '\n')
+ print('Initializing %s (Project: %s, Package: %s)' % (os.curdir, project, package))
+
+ @cmdln.alias('ls')
+ @cmdln.alias('ll')
+ @cmdln.alias('lL')
+ @cmdln.alias('LL')
+ @cmdln.option('-a', '--arch', metavar='ARCH',
+ help='specify architecture (only for binaries)')
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='specify repository (only for binaries)')
+ @cmdln.option('-b', '--binaries', action='store_true',
+ help='list built binaries instead of sources')
+ @cmdln.option('-e', '--expand', action='store_true',
+ help='expand linked package (only for sources)')
+ @cmdln.option('-u', '--unexpand', action='store_true',
+ help='always work with unexpanded (source) packages')
+ @cmdln.option('-v', '--verbose', action='store_true',
+ help='print extra information')
+ @cmdln.option('-l', '--long', action='store_true', dest='verbose',
+ help='print extra information')
+ @cmdln.option('-D', '--deleted', action='store_true',
+ help='show only the former deleted projects or packages')
+ @cmdln.option('-M', '--meta', action='store_true',
+ help='list meta data files')
+ @cmdln.option('-R', '--revision', metavar='REVISION',
+ help='specify revision (only for sources)')
+ def do_list(self, subcmd, opts, *args):
+ """${cmd_name}: List sources or binaries on the server
+
+ Examples for listing sources:
+ ls # list all projects (deprecated)
+ ls / # list all projects
+ ls . # take PROJECT/PACKAGE from current dir.
+ ls PROJECT # list packages in a project
+ ls PROJECT PACKAGE # list source files of package of a project
+ ls PROJECT PACKAGE <file> # list <file> if this file exists
+ ls -v PROJECT PACKAGE # verbosely list source files of package
+ ls -l PROJECT PACKAGE # verbosely list source files of package
+ ll PROJECT PACKAGE # verbosely list source files of package
+ LL PROJECT PACKAGE # verbosely list source files of expanded link
+
+ With --verbose, the following fields will be shown for each item:
+ MD5 hash of file
+ Revision number of the last commit
+ Size (in bytes)
+ Date and time of the last commit
+
+ Examples for listing binaries:
+ ls -b PROJECT # list all binaries of a project
+ ls -b PROJECT -a ARCH # list ARCH binaries of a project
+ ls -b PROJECT -r REPO # list binaries in REPO
+ ls -b PROJECT PACKAGE REPO ARCH
+
+ Usage:
+ ${cmd_name} [PROJECT [PACKAGE]]
+ ${cmd_name} -b [PROJECT [PACKAGE [REPO [ARCH]]]]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ if subcmd == 'll':
+ opts.verbose = True
+ if subcmd == 'lL' or subcmd == 'LL':
+ opts.verbose = True
+ opts.expand = True
+
+ project = None
+ package = None
+ fname = None
+ if len(args) == 0:
+ # For consistency with *all* other commands
+ # this lists what the server has in the current wd.
+ # CAUTION: 'osc ls -b' already works like this.
+ pass
+ if len(args) > 0:
+ project = args[0]
+ if project == '/':
+ project = None
+ if project == '.':
+ cwd = os.getcwd()
+ if is_project_dir(cwd):
+ project = store_read_project(cwd)
+ elif is_package_dir(cwd):
+ project = store_read_project(cwd)
+ package = store_read_package(cwd)
+ if len(args) > 1:
+ package = args[1]
+ if len(args) > 2:
+ if opts.deleted:
+ raise oscerr.WrongArgs("Too many arguments when listing deleted packages")
+ if opts.binaries:
+ if opts.repo:
+ if opts.repo != args[2]:
+ raise oscerr.WrongArgs("conflicting repos specified ('%s' vs '%s')"%(opts.repo, args[2]))
+ else:
+ opts.repo = args[2]
+ else:
+ fname = args[2]
+
+ if len(args) > 3:
+ if not opts.binaries:
+ raise oscerr.WrongArgs('Too many arguments')
+ if opts.arch:
+ if opts.arch != args[3]:
+ raise oscerr.WrongArgs("conflicting archs specified ('%s' vs '%s')"%(opts.arch, args[3]))
+ else:
+ opts.arch = args[3]
+
+
+ if opts.binaries and opts.expand:
+ raise oscerr.WrongOptions('Sorry, --binaries and --expand are mutual exclusive.')
+
+ apiurl = self.get_api_url()
+
+ # list binaries
+ if opts.binaries:
+ # ls -b toplevel doesn't make sense, so use info from
+ # current dir if available
+ if len(args) == 0:
+ cwd = os.getcwd()
+ if is_project_dir(cwd):
+ project = store_read_project(cwd)
+ elif is_package_dir(cwd):
+ project = store_read_project(cwd)
+ package = store_read_package(cwd)
+
+ if not project:
+ raise oscerr.WrongArgs('There are no binaries to list above project level.')
+ if opts.revision:
+ raise oscerr.WrongOptions('Sorry, the --revision option is not supported for binaries.')
+
+ repos = []
+
+ if opts.repo and opts.arch:
+ repos.append(Repo(opts.repo, opts.arch))
+ elif opts.repo and not opts.arch:
+ repos = [repo for repo in get_repos_of_project(apiurl, project) if repo.name == opts.repo]
+ elif opts.arch and not opts.repo:
+ repos = [repo for repo in get_repos_of_project(apiurl, project) if repo.arch == opts.arch]
+ else:
+ repos = get_repos_of_project(apiurl, project)
+
+ results = []
+ for repo in repos:
+ results.append((repo, get_binarylist(apiurl, project, repo.name, repo.arch, package=package, verbose=opts.verbose)))
+
+ for result in results:
+ indent = ''
+ if len(results) > 1:
+ print('%s/%s' % (result[0].name, result[0].arch))
+ indent = ' '
+
+ if opts.verbose:
+ for f in result[1]:
+ print("%9d %s %-40s" % (f.size, shorttime(f.mtime), f.name))
+ else:
+ for f in result[1]:
+ print(indent+f)
+
+ # list sources
+ elif not opts.binaries:
+ if not args:
+ for prj in meta_get_project_list(apiurl, opts.deleted):
+ print(prj)
+
+ elif len(args) == 1:
+ if opts.verbose:
+ if self.options.verbose:
+ print('Sorry, the --verbose option is not implemented for projects.', file=sys.stderr)
+ for pkg in meta_get_packagelist(apiurl, project, deleted = opts.deleted, expand = opts.expand):
+ print(pkg)
+
+ elif len(args) == 2 or len(args) == 3:
+ link_seen = False
+ print_not_found = True
+ rev = opts.revision
+ for i in [ 1, 2 ]:
+ l = meta_get_filelist(apiurl,
+ project,
+ package,
+ verbose=opts.verbose,
+ expand=opts.expand,
+ meta=opts.meta,
+ deleted=opts.deleted,
+ revision=rev)
+ link_seen = '_link' in l
+ if opts.verbose:
+ out = [ '%s %7s %9d %s %s' % (i.md5, i.rev, i.size, shorttime(i.mtime), i.name) \
+ for i in l if not fname or fname == i.name ]
+ if len(out) > 0:
+ print_not_found = False
+ print('\n'.join(out))
+ elif fname:
+ if fname in l:
+ print(fname)
+ print_not_found = False
+ else:
+ print('\n'.join(l))
+ if opts.expand or opts.unexpand or not link_seen:
+ break
+ m = show_files_meta(apiurl, project, package)
+ li = Linkinfo()
+ li.read(ET.fromstring(''.join(m)).find('linkinfo'))
+ if li.haserror():
+ raise oscerr.LinkExpandError(project, package, li.error)
+ project, package, rev = li.project, li.package, li.rev
+ if rev:
+ print('# -> %s %s (%s)' % (project, package, rev))
+ else:
+ print('# -> %s %s (latest)' % (project, package))
+ opts.expand = True
+ if fname and print_not_found:
+ print('file \'%s\' does not exist' % fname)
+
+
+ @cmdln.option('-s', '--skip-disabled', action='store_true',
+ help='Skip disabled channels. Otherwise the source gets added, but not the repositories.')
+ @cmdln.option('-e', '--enable-all', action='store_true',
+ help='Enable all added channels including the ones disabled by default.')
+ def do_addchannels(self, subcmd, opts, *args):
+ """${cmd_name}: Add channels to project.
+
+ The command adds all channels which are defined to be used for a given source package.
+ The source link target is used to lookup the channels. The command can be
+ used for a certain package or for all in the specified project.
+
+ In case no channel is defined the operation is just returning.
+
+ Examples:
+ osc addchannels [PROJECT [PACKAGE]]
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ localdir = os.getcwd()
+ channel = None
+ if not args:
+ if is_project_dir(localdir) or is_package_dir(localdir):
+ project = store_read_project(localdir)
+ elif is_package_dir(localdir):
+ project = store_read_project(localdir)
+ channel = store_read_package(localdir)
+ else:
+ raise oscerr.WrongArgs('Either specify project [package] or call it from a project/package working copy')
+ else:
+ project = args[0]
+
+ query = {'cmd': 'addchannels'}
+
+ if opts.enable_all and opts.skip_disabled:
+ raise oscerr.WrongOptions('--enable-all and --skip-disabled options are mutually exclusive')
+ elif opts.enable_all:
+ query['mode'] = 'enable_all'
+ elif opts.skip_disabled:
+ query['mode'] = 'skip_disabled'
+
+ print("Looking for channels...")
+ url = makeurl(apiurl, ['source', project], query=query)
+ if channel:
+ url = makeurl(apiurl, ['source', project, channel], query=query)
+ f = http_POST(url)
+
+ @cmdln.alias('enablechannel')
+ def do_enablechannels(self, subcmd, opts, *args):
+ """${cmd_name}: Enables channels
+
+ Enables existing channel packages in a project. Enabling means adding the
+ needed repositories for building.
+ The command can be used to enable a specific one or all channels of a project.
+
+ Examples:
+ osc enablechannels [PROJECT [CHANNEL_PACKAGE]]
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ localdir = os.getcwd()
+ channel = None
+ if not args:
+ if is_project_dir(localdir):
+ project = store_read_project(localdir)
+ elif is_package_dir(localdir):
+ project = store_read_project(localdir)
+ channel = store_read_package(localdir)
+ else:
+ raise oscerr.WrongArgs('Either specify project [package] or call it from a project/package working copy')
+ else:
+ project = args[0]
+ if len(args) > 1:
+ channel = args[1]
+
+ query = {}
+ if channel:
+ query['cmd'] = 'enablechannel'
+ else:
+ query = {'cmd': 'modifychannels', 'mode': 'enable_all'}
+
+ print("Enable channel(s)...")
+ url = makeurl(apiurl, ['source', project], query=query)
+ if channel:
+ url = makeurl(apiurl, ['source', project, channel], query=query)
+ f = http_POST(url)
+
+ @cmdln.option('-f', '--force', action='store_true',
+ help='force generation of new patchinfo file, do not update existing one.')
+ def do_patchinfo(self, subcmd, opts, *args):
+ """${cmd_name}: Generate and edit a patchinfo file.
+
+ A patchinfo file describes the packages for an update and the kind of
+ problem it solves.
+
+ This command either creates a new _patchinfo or updates an existing one.
+
+ Examples:
+ osc patchinfo
+ osc patchinfo [PROJECT [PATCH_NAME]]
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ project_dir = localdir = os.getcwd()
+ patchinfo = 'patchinfo'
+ if len(args) == 0:
+ if is_project_dir(localdir):
+ project = store_read_project(localdir)
+ apiurl = self.get_api_url()
+ for p in meta_get_packagelist(apiurl, project):
+ if p.startswith("_patchinfo") or p.startswith("patchinfo"):
+ patchinfo = p
+ else:
+ if is_package_dir(localdir):
+ project = store_read_project(localdir)
+ patchinfo = store_read_package(localdir)
+ apiurl = self.get_api_url()
+ if not os.path.exists('_patchinfo'):
+ sys.exit('Current checked out package has no _patchinfo. Either call it from project level or specify patch name.')
+ else:
+ sys.exit('This command must be called in a checked out project or patchinfo package.')
+ else:
+ project = args[0]
+ if len(args) > 1:
+ patchinfo = args[1]
+
+ filelist = None
+ if patchinfo:
+ try:
+ filelist = meta_get_filelist(apiurl, project, patchinfo)
+ except HTTPError:
+ pass
+
+ if opts.force or not filelist or not '_patchinfo' in filelist:
+ print("Creating new patchinfo...")
+ query = 'cmd=createpatchinfo&name=' + patchinfo
+ if opts.force:
+ query += "&force=1"
+ url = makeurl(apiurl, ['source', project], query=query)
+ f = http_POST(url)
+ for p in meta_get_packagelist(apiurl, project):
+ if p.startswith("_patchinfo") or p.startswith("patchinfo"):
+ patchinfo = p
+ else:
+ print("Update existing _patchinfo file...")
+ query = 'cmd=updatepatchinfo'
+ url = makeurl(apiurl, ['source', project, patchinfo], query=query)
+ f = http_POST(url)
+
+ # CAUTION:
+ # Both conf.config['checkout_no_colon'] and conf.config['checkout_rooted']
+ # fool this test:
+ if is_package_dir(localdir):
+ pac = Package(localdir)
+ pac.update()
+ filename = "_patchinfo"
+ else:
+ checkout_package(apiurl, project, patchinfo, prj_dir=project_dir)
+ filename = project_dir + "/" + patchinfo + "/_patchinfo"
+
+ run_editor(filename)
+
+ @cmdln.alias('bsdevelproject')
+ @cmdln.alias('dp')
+ @cmdln.option('-r', '--raw', action='store_true', help='deprecated option')
+ def do_develproject(self, subcmd, opts, *args):
+ """${cmd_name}: print the devel project / package of a package
+
+ Examples:
+ osc develproject PRJ PKG
+ osc develproject
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ if len(args) == 0:
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ elif len(args) == 2:
+ project = args[0]
+ package = args[1]
+ else:
+ raise oscerr.WrongArgs('need Project and Package')
+
+ devprj, devpkg = show_devel_project(apiurl, project, package)
+ if devprj is None:
+ print('%s / %s has no devel project' % (project, package))
+ elif devpkg and devpkg != package:
+ print("%s %s" % (devprj, devpkg))
+ else:
+ print(devprj)
+
+ @cmdln.alias('sdp')
+ @cmdln.option('-u', '--unset', action='store_true',
+ help='remove devel project')
+ def do_setdevelproject(self, subcmd, opts, *args):
+ """${cmd_name}: Set the devel project / package of a package
+
+ Examples:
+ osc setdevelproject [PRJ PKG] DEVPRJ [DEVPKG]
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ devprj, devpkg = None, None
+ if len(args) == 3 or len(args) == 4:
+ project, package = args[0], args[1]
+ devprj = args[2]
+ if len(args) == 4:
+ devpkg = args[3]
+ elif len(args) >= 1 and len(args) <= 2:
+ project, package = store_read_project(os.curdir), store_read_package(os.curdir)
+ devprj = args[0]
+ if len(args) == 2:
+ devpkg = args[1]
+ else:
+ if opts.unset:
+ project, package = store_read_project(os.curdir), store_read_package(os.curdir)
+ else:
+ raise oscerr.WrongArgs('need at least DEVPRJ (and possibly DEVPKG)')
+
+ set_devel_project(apiurl, project, package, devprj, devpkg)
+
+
+ @cmdln.option('-c', '--create', action='store_true',
+ help='Create a new token')
+ @cmdln.option('-d', '--delete', metavar='TOKENID',
+ help='Create a new token')
+ @cmdln.option('-t', '--trigger', metavar='TOKENID',
+ help='Trigger the action of a token')
+ def do_token(self, subcmd, opts, *args):
+ """${cmd_name}: Show and manage authentication token
+
+ Authentication token can be used to run specific commands without
+ sending credentials.
+
+ Usage:
+ osc token
+ osc token --create [<PROJECT> <PACKAGE>]
+ osc token --delete <TOKENID>
+ osc token --trigger <TOKENID>
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ apiurl = self.get_api_url()
+ url = apiurl + "/person/" + conf.get_apiurl_usr(apiurl) + "/token"
+
+ if opts.create:
+ print("Create a new token")
+ url += "?cmd=create"
+ if len(args) > 1:
+ url += "&project=" + args[0]
+ url += "&package=" + args[1]
+
+ f = http_POST(url)
+ while True:
+ buf = f.read(16384)
+ if not buf:
+ break
+ sys.stdout.write(buf)
+
+ elif opts.delete:
+ print("Delete token")
+ url += "/" + opts.delete
+ http_DELETE(url)
+ elif opts.trigger:
+ print("Trigger token")
+ url = apiurl + "/trigger/runservice"
+ req = URLRequest(url)
+ req.get_method = lambda: "POST"
+ req.add_header('Content-Type', 'application/octet-stream')
+ req.add_header('Authorization', "Token "+opts.trigger)
+ fd = urlopen(req, data=None)
+ print(fd.read())
+ else:
+ # just list token
+ for data in streamfile(url, http_GET):
+ sys.stdout.write(data)
+
+
+ @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
+ help='affect only a given attribute')
+ @cmdln.option('--attribute-defaults', action='store_true',
+ help='include defined attribute defaults')
+ @cmdln.option('--attribute-project', action='store_true',
+ help='include project values, if missing in packages ')
+ @cmdln.option('-f', '--force', action='store_true',
+ help='force the save operation, allows one to ignores some errors like depending repositories. For prj meta only.')
+ @cmdln.option('-F', '--file', metavar='FILE',
+ help='read metadata from FILE, instead of opening an editor. '
+ '\'-\' denotes standard input. ')
+ @cmdln.option('-e', '--edit', action='store_true',
+ help='edit metadata')
+ @cmdln.option('-c', '--create', action='store_true',
+ help='create attribute without values')
+ @cmdln.option('-R', '--remove-linking-repositories', action='store_true',
+ help='Try to remove also all repositories building against remove ones.')
+ @cmdln.option('-s', '--set', metavar='ATTRIBUTE_VALUES',
+ help='set attribute values')
+ @cmdln.option('--delete', action='store_true',
+ help='delete a pattern or attribute')
+ def do_meta(self, subcmd, opts, *args):
+ """${cmd_name}: Show meta information, or edit it
+
+ Show or edit build service metadata of type <prj|pkg|prjconf|user|pattern>.
+
+ This command displays metadata on buildservice objects like projects,
+ packages, or users. The type of metadata is specified by the word after
+ "meta", like e.g. "meta prj".
+
+ prj denotes metadata of a buildservice project.
+ prjconf denotes the (build) configuration of a project.
+ pkg denotes metadata of a buildservice package.
+ user denotes the metadata of a user.
+ pattern denotes installation patterns defined for a project.
+
+ To list patterns, use 'osc meta pattern PRJ'. An additional argument
+ will be the pattern file to view or edit.
+
+ With the --edit switch, the metadata can be edited. Per default, osc
+ opens the program specified by the environmental variable EDITOR with a
+ temporary file. Alternatively, content to be saved can be supplied via
+ the --file switch. If the argument is '-', input is taken from stdin:
+ osc meta prjconf home:user | sed ... | osc meta prjconf home:user -F -
+
+ When trying to edit a non-existing resource, it is created implicitly.
+
+
+ Examples:
+ osc meta prj PRJ
+ osc meta pkg PRJ PKG
+ osc meta pkg PRJ PKG -e
+
+ Usage:
+ osc meta <prj|pkg|prjconf|user|pattern> ARGS...
+ osc meta <prj|pkg|prjconf|user|pattern> -e|--edit ARGS...
+ osc meta <prj|pkg|prjconf|user|pattern> -F|--file ARGS...
+ osc meta pattern --delete PRJ PATTERN
+ osc meta attribute PRJ [PKG [SUBPACKAGE]] [--attribute ATTRIBUTE] [--create|--delete|--set [value_list]]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ if not args or args[0] not in metatypes.keys():
+ raise oscerr.WrongArgs('Unknown meta type. Choose one of %s.' \
+ % ', '.join(metatypes))
+
+ cmd = args[0]
+ del args[0]
+
+ if cmd in ['pkg']:
+ min_args, max_args = 0, 2
+ elif cmd in ['pattern']:
+ min_args, max_args = 1, 2
+ elif cmd in ['attribute']:
+ min_args, max_args = 1, 3
+ elif cmd in ['prj', 'prjconf']:
+ min_args, max_args = 0, 1
+ else:
+ min_args, max_args = 1, 1
+
+ if len(args) < min_args:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) > max_args:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ apiurl = self.get_api_url()
+
+ # Specific arguments
+ #
+ # If project or package arguments missing, assume to work
+ # with project and/or package in current local directory.
+ attributepath = []
+ if cmd in ['prj', 'prjconf']:
+ if len(args) < 1:
+ apiurl = store_read_apiurl(os.curdir)
+ project = store_read_project(os.curdir)
+ else:
+ project = args[0]
+
+ elif cmd == 'pkg':
+ if len(args) < 2:
+ apiurl = store_read_apiurl(os.curdir)
+ project = store_read_project(os.curdir)
+ if len(args) < 1:
+ package = store_read_package(os.curdir)
+ else:
+ package = args[0]
+ else:
+ project = args[0]
+ package = args[1]
+
+ elif cmd == 'attribute':
+ project = args[0]
+ if len(args) > 1:
+ package = args[1]
+ else:
+ package = None
+ if opts.attribute_project:
+ raise oscerr.WrongOptions('--attribute-project works only when also a package is given')
+ if len(args) > 2:
+ subpackage = args[2]
+ else:
+ subpackage = None
+ attributepath.append('source')
+ attributepath.append(project)
+ if package:
+ attributepath.append(package)
+ if subpackage:
+ attributepath.append(subpackage)
+ attributepath.append('_attribute')
+ elif cmd == 'user':
+ user = args[0]
+ elif cmd == 'pattern':
+ project = args[0]
+ if len(args) > 1:
+ pattern = args[1]
+ else:
+ pattern = None
+ # enforce pattern argument if needed
+ if opts.edit or opts.file:
+ raise oscerr.WrongArgs('A pattern file argument is required.')
+
+ # show
+ if not opts.edit and not opts.file and not opts.delete and not opts.create and not opts.set:
+ if cmd == 'prj':
+ sys.stdout.write(''.join(show_project_meta(apiurl, project)))
+ elif cmd == 'pkg':
+ sys.stdout.write(''.join(show_package_meta(apiurl, project, package)))
+ elif cmd == 'attribute':
+ sys.stdout.write(''.join(show_attribute_meta(apiurl, project, package, subpackage,
+ opts.attribute, opts.attribute_defaults, opts.attribute_project)))
+ elif cmd == 'prjconf':
+ sys.stdout.write(''.join(show_project_conf(apiurl, project)))
+ elif cmd == 'user':
+ r = get_user_meta(apiurl, user)
+ if r:
+ sys.stdout.write(''.join(r))
+ elif cmd == 'pattern':
+ if pattern:
+ r = show_pattern_meta(apiurl, project, pattern)
+ if r:
+ sys.stdout.write(''.join(r))
+ else:
+ r = show_pattern_metalist(apiurl, project)
+ if r:
+ sys.stdout.write('\n'.join(r) + '\n')
+
+ # edit
+ if opts.edit and not opts.file:
+ if cmd == 'prj':
+ edit_meta(metatype='prj',
+ edit=True,
+ force=opts.force,
+ remove_linking_repositories=opts.remove_linking_repositories,
+ path_args=quote_plus(project),
+ apiurl=apiurl,
+ template_args=({
+ 'name': project,
+ 'user': conf.get_apiurl_usr(apiurl)}))
+ elif cmd == 'pkg':
+ edit_meta(metatype='pkg',
+ edit=True,
+ path_args=(quote_plus(project), quote_plus(package)),
+ apiurl=apiurl,
+ template_args=({
+ 'name': package,
+ 'user': conf.get_apiurl_usr(apiurl)}))
+ elif cmd == 'prjconf':
+ edit_meta(metatype='prjconf',
+ edit=True,
+ path_args=quote_plus(project),
+ apiurl=apiurl,
+ template_args=None)
+ elif cmd == 'user':
+ edit_meta(metatype='user',
+ edit=True,
+ path_args=(quote_plus(user)),
+ apiurl=apiurl,
+ template_args=({'user': user}))
+ elif cmd == 'pattern':
+ edit_meta(metatype='pattern',
+ edit=True,
+ path_args=(project, pattern),
+ apiurl=apiurl,
+ template_args=None)
+
+ # create attribute entry
+ if (opts.create or opts.set) and cmd == 'attribute':
+ if not opts.attribute:
+ raise oscerr.WrongOptions('no attribute given to create')
+ values = ''
+ if opts.set:
+ opts.set = opts.set.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
+ for i in opts.set.split(','):
+ values += '<value>%s</value>' % i
+ aname = opts.attribute.split(":")
+ if len(aname) != 2:
+ raise oscerr.WrongOptions('Given attribute is not in "NAMESPACE:NAME" style')
+ d = '<attributes><attribute namespace=\'%s\' name=\'%s\' >%s</attribute></attributes>' % (aname[0], aname[1], values)
+ url = makeurl(apiurl, attributepath)
+ for data in streamfile(url, http_POST, data=d):
+ sys.stdout.write(data)
+
+ # upload file
+ if opts.file:
+
+ if opts.file == '-':
+ f = sys.stdin.read()
+ else:
+ try:
+ f = open(opts.file).read()
+ except:
+ sys.exit('could not open file \'%s\'.' % opts.file)
+
+ if cmd == 'prj':
+ edit_meta(metatype='prj',
+ data=f,
+ edit=opts.edit,
+ force=opts.force,
+ remove_linking_repositories=opts.remove_linking_repositories,
+ apiurl=apiurl,
+ path_args=quote_plus(project))
+ elif cmd == 'pkg':
+ edit_meta(metatype='pkg',
+ data=f,
+ edit=opts.edit,
+ apiurl=apiurl,
+ path_args=(quote_plus(project), quote_plus(package)))
+ elif cmd == 'prjconf':
+ edit_meta(metatype='prjconf',
+ data=f,
+ edit=opts.edit,
+ apiurl=apiurl,
+ path_args=quote_plus(project))
+ elif cmd == 'user':
+ edit_meta(metatype='user',
+ data=f,
+ edit=opts.edit,
+ apiurl=apiurl,
+ path_args=(quote_plus(user)))
+ elif cmd == 'pattern':
+ edit_meta(metatype='pattern',
+ data=f,
+ edit=opts.edit,
+ apiurl=apiurl,
+ path_args=(project, pattern))
+
+
+ # delete
+ if opts.delete:
+ path = metatypes[cmd]['path']
+ if cmd == 'pattern':
+ path = path % (project, pattern)
+ u = makeurl(apiurl, [path])
+ http_DELETE(u)
+ elif cmd == 'attribute':
+ if not opts.attribute:
+ raise oscerr.WrongOptions('no attribute given to create')
+ attributepath.append(opts.attribute)
+ u = makeurl(apiurl, attributepath)
+ for data in streamfile(u, http_DELETE):
+ sys.stdout.write(data)
+ else:
+ raise oscerr.WrongOptions('The --delete switch is only for pattern metadata or attributes.')
+
+
+ # TODO: rewrite and consolidate the current submitrequest/createrequest "mess"
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-r', '--revision', metavar='REV',
+ help='specify a certain source revision ID (the md5 sum) for the source package')
+ @cmdln.option('-s', '--supersede', metavar='SUPERSEDE',
+ help='Superseding another request by this one')
+ @cmdln.option('--nodevelproject', action='store_true',
+ help='do not follow a defined devel project ' \
+ '(primary project where a package is developed)')
+ @cmdln.option('--seperate-requests', action='store_true',
+ help='Create multiple request instead of a single one (when command is used for entire project)')
+ @cmdln.option('--cleanup', action='store_true',
+ help='remove package if submission gets accepted (default for home:<id>:branch projects)')
+ @cmdln.option('--no-cleanup', action='store_true',
+ help='never remove source package on accept, but update its content')
+ @cmdln.option('--no-update', action='store_true',
+ help='never touch source package on accept (will break source links)')
+ @cmdln.option('--update-link', action='store_true',
+ help='This transfers the source including the _link file.')
+ @cmdln.option('-d', '--diff', action='store_true',
+ help='show diff only instead of creating the actual request')
+ @cmdln.option('--yes', action='store_true',
+ help='proceed without asking.')
+ @cmdln.alias("sr")
+ @cmdln.alias("submitreq")
+ @cmdln.alias("submitpac")
+ def do_submitrequest(self, subcmd, opts, *args):
+ """${cmd_name}: Create request to submit source into another Project
+
+ [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration for information
+ on this topic.]
+
+ See the "request" command for showing and modifing existing requests.
+
+ usage:
+ osc submitreq [OPTIONS]
+ osc submitreq [OPTIONS] DESTPRJ [DESTPKG]
+ osc submitreq [OPTIONS] SOURCEPRJ SOURCEPKG DESTPRJ [DESTPKG]
+
+ osc submitpac ... is a shorthand for osc submitreq --cleanup ...
+
+ ${cmd_option_list}
+ """
+
+ if opts.cleanup and opts.no_cleanup:
+ raise oscerr.WrongOptions('\'--cleanup\' and \'--no-cleanup\' are mutually exclusive')
+
+ src_update = conf.config['submitrequest_on_accept_action'] or None
+ # we should check here for home:<id>:branch and default to update, but that would require OBS 1.7 server
+
+ if subcmd == 'submitpac' and not opts.no_cleanup:
+ opts.cleanup = True
+
+ if opts.cleanup:
+ src_update = "cleanup"
+ elif opts.no_cleanup:
+ src_update = "update"
+ elif opts.no_update:
+ src_update = "noupdate"
+
+ myreqs = []
+ if opts.supersede:
+ myreqs = [opts.supersede]
+
+ args = slash_split(args)
+
+ # remove this block later again
+ oldcmds = ['create', 'list', 'log', 'show', 'decline', 'accept', 'delete', 'revoke']
+ if args and args[0] in oldcmds:
+ print("************************************************************************", file=sys.stderr)
+ print("* WARNING: It looks that you are using this command with a *", file=sys.stderr)
+ print("* deprecated syntax. *", file=sys.stderr)
+ print("* Please run \"osc sr --help\" and \"osc rq --help\" *", file=sys.stderr)
+ print("* to see the new syntax. *", file=sys.stderr)
+ print("************************************************************************", file=sys.stderr)
+ if args[0] == 'create':
+ args.pop(0)
+ else:
+ sys.exit(1)
+
+ if len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 2 and is_project_dir(os.getcwd()):
+ sys.exit('You can not specify a target package when submitting an entire project\n')
+
+ apiurl = self.get_api_url()
+
+ if len(args) < 2 and is_project_dir(os.getcwd()):
+ if opts.diff:
+ raise oscerr.WrongOptions('\'--diff\' is not supported in a project working copy')
+ import cgi
+ project = store_read_project(os.curdir)
+
+ sr_ids = []
+ # for single request
+ actionxml = ""
+ options_block = "<options>"
+ if src_update:
+ options_block += """<sourceupdate>%s</sourceupdate>""" % (src_update)
+ if opts.update_link:
+ options_block + """<updatelink>true</updatelink></options> """
+ options_block += "</options>"
+
+ # loop via all packages for checking their state
+ for p in meta_get_packagelist(apiurl, project):
+ # get _link info from server, that knows about the local state ...
+ u = makeurl(apiurl, ['source', project, p])
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ target_project = None
+ if len(args) == 1:
+ target_project = args[0]
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ if len(args) < 1:
+ print("Package ", p, " is not a source link and no target specified.")
+ sys.exit("This is currently not supported.")
+ else:
+ if linkinfo.get('error'):
+ print("Package ", p, " is a broken source link.")
+ sys.exit("Please fix this first")
+ t = linkinfo.get('project')
+ if t:
+ if target_project == None:
+ target_project = t
+ if len(root.findall('entry')) > 1: # This is not really correct, but should work mostly
+ # Real fix is to ask the api if sources are modificated
+ # but there is no such call yet.
+ print("Submitting package ", p)
+ else:
+ print(" Skipping not modified package ", p)
+ continue
+ else:
+ print("Skipping package ", p, " since it is a source link pointing inside the project.")
+ continue
+
+ serviceinfo = root.find('serviceinfo')
+ if serviceinfo != None:
+ if serviceinfo.get('code') != "succeeded":
+ print("Package ", p, " has a ", serviceinfo.get('code'), " source service")
+ sys.exit("Please fix this first")
+ if serviceinfo.get('error'):
+ print("Package ", p, " contains a failed source service.")
+ sys.exit("Please fix this first")
+
+ # submitting this package
+ if opts.seperate_requests:
+ # create a single request
+ result = create_submit_request(apiurl, project, p)
+ if not result:
+ sys.exit("submit request creation failed")
+ sr_ids.append(result)
+ else:
+ s = """<action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> %s </action>""" % \
+ (project, p, t, p, options_block)
+ actionxml += s
+
+ if actionxml != "":
+ xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \
+ (actionxml, cgi.escape(opts.message or ""))
+ u = makeurl(apiurl, ['request'], query='cmd=create&addrevision=1')
+ f = http_POST(u, data=xml)
+
+ root = ET.parse(f).getroot()
+ sr_ids.append(root.get('id'))
+
+ print("Request created: ", end=' ')
+ for i in sr_ids:
+ print(i, end=' ')
+
+ # was this project created by clone request ?
+ u = makeurl(apiurl, ['source', project, '_attribute', 'OBS:RequestCloned'])
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ value = root.findtext('attribute/value')
+ if value and not opts.yes:
+ repl = ''
+ print('\n\nThere are already following submit request: %s.' % \
+ ', '.join([str(i) for i in myreqs ]))
+ repl = raw_input('\nSupersede the old requests? (y/n) ')
+ if repl.lower() == 'y':
+ myreqs += [ value ]
+
+ if len(myreqs) > 0:
+ for req in myreqs:
+ change_request_state(apiurl, str(req), 'superseded',
+ 'superseded by %s' % result, result)
+
+ sys.exit('Successfully finished')
+
+ elif len(args) <= 2:
+ # try using the working copy at hand
+ p = findpacs(os.curdir)[0]
+ src_project = p.prjname
+ src_package = p.name
+ apiurl = p.apiurl
+ if len(args) == 0 and p.islink():
+ dst_project = p.linkinfo.project
+ dst_package = p.linkinfo.package
+ elif len(args) > 0:
+ dst_project = args[0]
+ if len(args) == 2:
+ dst_package = args[1]
+ else:
+ if p.islink():
+ dst_package = p.linkinfo.package
+ else:
+ dst_package = src_package
+ else:
+ sys.exit('Package \'%s\' is not a source link, so I cannot guess the submit target.\n'
+ 'Please provide it the target via commandline arguments.' % p.name)
+
+ modified = [i for i in p.filenamelist if not p.status(i) in (' ', '?', 'S')]
+ if len(modified) > 0 and not opts.yes:
+ print('Your working copy has local modifications.')
+ repl = raw_input('Proceed without committing the local changes? (y|N) ')
+ if repl != 'y':
+ raise oscerr.UserAbort()
+ elif len(args) >= 3:
+ # get the arguments from the commandline
+ src_project, src_package, dst_project = args[0:3]
+ if len(args) == 4:
+ dst_package = args[3]
+ else:
+ dst_package = src_package
+ else:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('request'))
+
+ # check for running source service
+ u = makeurl(apiurl, ['source', src_project, src_package])
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ serviceinfo = root.find('serviceinfo')
+ if serviceinfo != None:
+ if serviceinfo.get('code') != "succeeded":
+ print("Package ", src_package, " has a ", serviceinfo.get('code'), " source service")
+ sys.exit("Please fix this first")
+ if serviceinfo.get('error'):
+ print("Package ", src_package, " contains a failed source service.")
+ sys.exit("Please fix this first")
+
+ if not opts.nodevelproject:
+ devloc = None
+ try:
+ devloc, _ = show_devel_project(apiurl, dst_project, dst_package)
+ except HTTPError:
+ print("""\
+Warning: failed to fetch meta data for '%s' package '%s' (new package?) """ \
+ % (dst_project, dst_package), file=sys.stderr)
+
+ if devloc and \
+ dst_project != devloc and \
+ src_project != devloc:
+ print("""\
+A different project, %s, is defined as the place where development
+of the package %s primarily takes place.
+Please submit there instead, or use --nodevelproject to force direct submission.""" \
+ % (devloc, dst_package))
+ if not opts.diff:
+ sys.exit(1)
+
+ rev = opts.revision
+ if not rev:
+ # get _link info from server, that knows about the local state ...
+ u = makeurl(apiurl, ['source', src_project, src_package], query="expand=1")
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ rev = root.get('rev')
+ else:
+ if linkinfo.get('project') != dst_project or linkinfo.get('package') != dst_package:
+ # the submit target is not link target. use merged md5sum references to
+ # avoid not mergable sources when multiple request from same source get created.
+ rev = root.get('srcmd5')
+
+ rdiff = None
+ if opts.diff or not opts.message:
+ try:
+ rdiff = 'old: %s/%s\nnew: %s/%s rev %s\n' % (dst_project, dst_package, src_project, src_package, rev)
+ rdiff += server_diff(apiurl,
+ dst_project, dst_package, None,
+ src_project, src_package, rev, True)
+ except:
+ rdiff = ''
+
+ if opts.diff:
+ run_pager(rdiff)
+ return
+ supersede_existing = False
+ reqs = []
+ if not opts.supersede:
+ (supersede_existing, reqs) = check_existing_requests(apiurl,
+ src_project,
+ src_package,
+ dst_project,
+ dst_package)
+ if not supersede_existing:
+ (supersede_existing, reqs) = check_existing_maintenance_requests(apiurl,
+ src_project,
+ [src_package],
+ dst_project, None)
+ if not opts.message:
+ difflines = []
+ doappend = False
+ changes_re = re.compile(r'^--- .*\.changes ')
+ for line in rdiff.split('\n'):
+ if line.startswith('--- '):
+ if changes_re.match(line):
+ doappend = True
+ else:
+ doappend = False
+ if doappend:
+ difflines.append(line)
+ opts.message = edit_message(footer=rdiff, template='\n'.join(parse_diff_for_commit_message('\n'.join(difflines))))
+
+ result = create_submit_request(apiurl,
+ src_project, src_package,
+ dst_project, dst_package,
+ opts.message, orev=rev,
+ src_update=src_update, dst_updatelink=opts.update_link)
+ print('created request id', result)
+
+ if supersede_existing:
+ for req in reqs:
+ change_request_state(apiurl, req.reqid, 'superseded',
+ 'superseded by %s' % result, result)
+
+ if opts.supersede:
+ change_request_state(apiurl, opts.supersede, 'superseded',
+ opts.message or '', result)
+
+ def _actionparser(self, opt_str, value, parser):
+ value = []
+ if not hasattr(parser.values, 'actiondata'):
+ setattr(parser.values, 'actiondata', [])
+ if parser.values.actions == None:
+ parser.values.actions = []
+
+ rargs = parser.rargs
+ while rargs:
+ arg = rargs[0]
+ if ((arg[:2] == "--" and len(arg) > 2) or
+ (arg[:1] == "-" and len(arg) > 1 and arg[1] != "-")):
+ break
+ else:
+ value.append(arg)
+ del rargs[0]
+
+ parser.values.actions.append(value[0])
+ del value[0]
+ parser.values.actiondata.append(value)
+
+ def _submit_request(self, args, opts, options_block):
+ actionxml = ""
+ apiurl = self.get_api_url()
+ if len(args) == 0 and is_project_dir(os.getcwd()):
+ # submit requests for multiple packages are currently handled via multiple requests
+ # They could be also one request with multiple actions, but that avoids to accepts parts of it.
+ project = store_read_project(os.curdir)
+
+ pi = []
+ pac = []
+ targetprojects = []
+ # loop via all packages for checking their state
+ for p in meta_get_packagelist(apiurl, project):
+ if p.startswith("_patchinfo:"):
+ pi.append(p)
+ else:
+ # get _link info from server, that knows about the local state ...
+ u = makeurl(apiurl, ['source', project, p])
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ print("Package ", p, " is not a source link.")
+ sys.exit("This is currently not supported.")
+ if linkinfo.get('error'):
+ print("Package ", p, " is a broken source link.")
+ sys.exit("Please fix this first")
+ t = linkinfo.get('project')
+ if t:
+ rdiff = ''
+ try:
+ rdiff = server_diff(apiurl, t, p, opts.revision, project, p, None, True)
+ except:
+ rdiff = ''
+
+ if rdiff != '':
+ targetprojects.append(t)
+ pac.append(p)
+ else:
+ print("Skipping package ", p, " since it has no difference with the target package.")
+ else:
+ print("Skipping package ", p, " since it is a source link pointing inside the project.")
+
+ # loop via all packages to do the action
+ for p in pac:
+ s = """<action type="submit"> <source project="%s" package="%s" rev="%s"/> <target project="%s" package="%s"/> %s </action>""" % \
+ (project, p, opts.revision or show_upstream_rev(apiurl, project, p), t, p, options_block)
+ actionxml += s
+
+ # create submit requests for all found patchinfos
+ for p in pi:
+ for t in targetprojects:
+ s = """<action type="submit"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> %s </action>""" % \
+ (project, p, t, p, options_block)
+ actionxml += s
+
+ return actionxml, []
+
+ elif len(args) <= 2:
+ # try using the working copy at hand
+ p = findpacs(os.curdir)[0]
+ src_project = p.prjname
+ src_package = p.name
+ if len(args) == 0 and p.islink():
+ dst_project = p.linkinfo.project
+ dst_package = p.linkinfo.package
+ elif len(args) > 0:
+ dst_project = args[0]
+ if len(args) == 2:
+ dst_package = args[1]
+ else:
+ dst_package = src_package
+ else:
+ sys.exit('Package \'%s\' is not a source link, so I cannot guess the submit target.\n'
+ 'Please provide it the target via commandline arguments.' % p.name)
+
+ modified = [i for i in p.filenamelist if p.status(i) != ' ' and p.status(i) != '?']
+ if len(modified) > 0:
+ print('Your working copy has local modifications.')
+ repl = raw_input('Proceed without committing the local changes? (y|N) ')
+ if repl != 'y':
+ sys.exit(1)
+ elif len(args) >= 3:
+ # get the arguments from the commandline
+ src_project, src_package, dst_project = args[0:3]
+ if len(args) == 4:
+ dst_package = args[3]
+ else:
+ dst_package = src_package
+ else:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('request'))
+
+ if not opts.nodevelproject:
+ devloc = None
+ try:
+ devloc, _ = show_devel_project(apiurl, dst_project, dst_package)
+ except HTTPError:
+ print("""\
+Warning: failed to fetch meta data for '%s' package '%s' (new package?) """ \
+ % (dst_project, dst_package), file=sys.stderr)
+
+ if devloc and \
+ dst_project != devloc and \
+ src_project != devloc:
+ print("""\
+A different project, %s, is defined as the place where development
+of the package %s primarily takes place.
+Please submit there instead, or use --nodevelproject to force direct submission.""" \
+ % (devloc, dst_package))
+ sys.exit(1)
+
+ reqs = get_request_list(apiurl, dst_project, dst_package, req_type='submit', req_state=['new', 'review'])
+ user = conf.get_apiurl_usr(apiurl)
+ myreqs = [ i for i in reqs if i.state.who == user and i.reqid != opts.supersede ]
+ repl = 'y'
+ if len(myreqs) > 0 and not opts.yes:
+ print('You already created the following submit request: %s.' % \
+ ', '.join([i.reqid for i in myreqs ]))
+ repl = raw_input('Supersede the old requests? (y/n/c) ')
+ if repl.lower() == 'c':
+ print('Aborting', file=sys.stderr)
+ sys.exit(1)
+ elif repl.lower() != 'y':
+ myreqs = []
+
+ actionxml = """<action type="submit"> <source project="%s" package="%s" rev="%s"/> <target project="%s" package="%s"/> %s </action>""" % \
+ (src_project, src_package, opts.revision or show_upstream_rev(apiurl, src_project, src_package), dst_project, dst_package, options_block)
+ if opts.supersede:
+ myreqs.append(opts.supersede)
+
+ #print 'created request id', result
+ return actionxml, myreqs
+
+ def _delete_request(self, args, opts):
+ if len(args) < 1:
+ raise oscerr.WrongArgs('Please specify at least a project.')
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ package = ""
+ if len(args) > 1:
+ package = """package="%s" """ % (args[1])
+ actionxml = """<action type="delete"> <target project="%s" %s/> </action> """ % (args[0], package)
+ return actionxml
+
+ def _changedevel_request(self, args, opts):
+ if len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 0 and is_package_dir('.') and find_default_project():
+ wd = os.curdir
+ devel_project = store_read_project(wd)
+ devel_package = package = store_read_package(wd)
+ project = find_default_project(self.get_api_url(), package)
+ else:
+ if len(args) < 3:
+ raise oscerr.WrongArgs('Too few arguments.')
+
+ devel_project = args[2]
+ project = args[0]
+ package = args[1]
+ devel_package = package
+ if len(args) > 3:
+ devel_package = args[3]
+
+ actionxml = """ <action type="change_devel"> <source project="%s" package="%s" /> <target project="%s" package="%s" /> </action> """ % \
+ (devel_project, devel_package, project, package)
+
+ return actionxml
+
+ def _add_me(self, args, opts):
+ if len(args) > 3:
+ raise oscerr.WrongArgs('Too many arguments.')
+ if len(args) < 2:
+ raise oscerr.WrongArgs('Too few arguments.')
+
+ apiurl = self.get_api_url()
+
+ user = conf.get_apiurl_usr(apiurl)
+ role = args[0]
+ project = args[1]
+ actionxml = """ <action type="add_role"> <target project="%s" /> <person name="%s" role="%s" /> </action> """ % \
+ (project, user, role)
+
+ if len(args) > 2:
+ package = args[2]
+ actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <person name="%s" role="%s" /> </action> """ % \
+ (project, package, user, role)
+
+ if get_user_meta(apiurl, user) == None:
+ raise oscerr.WrongArgs('osc: an error occured.')
+
+ return actionxml
+
+ def _add_user(self, args, opts):
+ if len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+ if len(args) < 3:
+ raise oscerr.WrongArgs('Too few arguments.')
+
+ apiurl = self.get_api_url()
+
+ user = args[0]
+ role = args[1]
+ project = args[2]
+ actionxml = """ <action type="add_role"> <target project="%s" /> <person name="%s" role="%s" /> </action> """ % \
+ (project, user, role)
+
+ if len(args) > 3:
+ package = args[3]
+ actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <person name="%s" role="%s" /> </action> """ % \
+ (project, package, user, role)
+
+ if get_user_meta(apiurl, user) == None:
+ raise oscerr.WrongArgs('osc: an error occured.')
+
+ return actionxml
+
+ def _add_group(self, args, opts):
+ if len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+ if len(args) < 3:
+ raise oscerr.WrongArgs('Too few arguments.')
+
+ apiurl = self.get_api_url()
+
+ group = args[0]
+ role = args[1]
+ project = args[2]
+ actionxml = """ <action type="add_role"> <target project="%s" /> <group name="%s" role="%s" /> </action> """ % \
+ (project, group, role)
+
+ if len(args) > 3:
+ package = args[3]
+ actionxml = """ <action type="add_role"> <target project="%s" package="%s" /> <group name="%s" role="%s" /> </action> """ % \
+ (project, package, group, role)
+
+ if get_group(apiurl, group) == None:
+ raise oscerr.WrongArgs('osc: an error occured.')
+
+ return actionxml
+
+ def _set_bugowner(self, args, opts):
+ if len(args) > 3:
+ raise oscerr.WrongArgs('Too many arguments.')
+ if len(args) < 2:
+ raise oscerr.WrongArgs('Too few arguments.')
+
+ apiurl = self.get_api_url()
+
+ user = args[0]
+ project = args[1]
+ package = ""
+ if len(args) > 2:
+ package = """package="%s" """ % (args[2])
+
+ if user.startswith('group:'):
+ group = user.replace('group:', '')
+ actionxml = """ <action type="set_bugowner"> <target project="%s" %s /> <group name="%s" /> </action> """ % \
+ (project, package, group)
+ if get_group(apiurl, group) == None:
+ raise oscerr.WrongArgs('osc: an error occured.')
+ else:
+ actionxml = """ <action type="set_bugowner"> <target project="%s" %s /> <person name="%s" /> </action> """ % \
+ (project, package, user)
+ if get_user_meta(apiurl, user) == None:
+ raise oscerr.WrongArgs('osc: an error occured.')
+
+
+ return actionxml
+
+ @cmdln.option('-a', '--action', action='callback', callback = _actionparser, dest = 'actions',
+ help='specify action type of a request, can be : submit/delete/change_devel/add_role/set_bugowner')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-r', '--revision', metavar='REV',
+ help='for "create", specify a certain source revision ID (the md5 sum)')
+ @cmdln.option('-s', '--supersede', metavar='SUPERSEDE',
+ help='Superseding another request by this one')
+ @cmdln.option('--nodevelproject', action='store_true',
+ help='do not follow a defined devel project ' \
+ '(primary project where a package is developed)')
+ @cmdln.option('--cleanup', action='store_true',
+ help='remove package if submission gets accepted (default for home:<id>:branch projects)')
+ @cmdln.option('--no-cleanup', action='store_true',
+ help='never remove source package on accept, but update its content')
+ @cmdln.option('--no-update', action='store_true',
+ help='never touch source package on accept (will break source links)')
+ @cmdln.option('--yes', action='store_true',
+ help='proceed without asking.')
+ @cmdln.alias("creq")
+ def do_createrequest(self, subcmd, opts, *args):
+ """${cmd_name}: create multiple requests with a single command
+
+ usage:
+ osc creq [OPTIONS] [
+ -a submit SOURCEPRJ SOURCEPKG DESTPRJ [DESTPKG]
+ -a delete PROJECT [PACKAGE]
+ -a change_devel PROJECT PACKAGE DEVEL_PROJECT [DEVEL_PACKAGE]
+ -a add_me ROLE PROJECT [PACKAGE]
+ -a add_group GROUP ROLE PROJECT [PACKAGE]
+ -a add_role USER ROLE PROJECT [PACKAGE]
+ -a set_bugowner USER PROJECT [PACKAGE]
+ ]
+
+ Option -m works for all types of request, the rest work only for submit.
+ example:
+ osc creq -a submit -a delete home:someone:branches:openSUSE:Tools -a change_devel openSUSE:Tools osc home:someone:branches:openSUSE:Tools -m ok
+
+ This will submit all modified packages under current directory, delete project home:someone:branches:openSUSE:Tools and change the devel project to home:someone:branches:openSUSE:Tools for package osc in project openSUSE:Tools.
+ ${cmd_option_list}
+ """
+ src_update = conf.config['submitrequest_on_accept_action'] or None
+ # we should check here for home:<id>:branch and default to update, but that would require OBS 1.7 server
+ if opts.cleanup:
+ src_update = "cleanup"
+ elif opts.no_cleanup:
+ src_update = "update"
+ elif opts.no_update:
+ src_update = "noupdate"
+
+ options_block = ""
+ if src_update:
+ options_block = """<options><sourceupdate>%s</sourceupdate></options> """ % (src_update)
+
+ args = slash_split(args)
+
+ apiurl = self.get_api_url()
+
+ i = 0
+ actionsxml = ""
+ supersede = []
+ for ai in opts.actions:
+ if ai == 'submit':
+ args = opts.actiondata[i]
+ i = i+1
+ actions, to_supersede = self._submit_request(args, opts, options_block)
+ actionsxml += actions
+ supersede.extend(to_supersede)
+ elif ai == 'delete':
+ args = opts.actiondata[i]
+ actionsxml += self._delete_request(args, opts)
+ i = i+1
+ elif ai == 'change_devel':
+ args = opts.actiondata[i]
+ actionsxml += self._changedevel_request(args, opts)
+ i = i+1
+ elif ai == 'add_me':
+ args = opts.actiondata[i]
+ actionsxml += self._add_me(args, opts)
+ i = i+1
+ elif ai == 'add_group':
+ args = opts.actiondata[i]
+ actionsxml += self._add_group(args, opts)
+ i = i+1
+ elif ai == 'add_role':
+ args = opts.actiondata[i]
+ actionsxml += self._add_user(args, opts)
+ i = i+1
+ elif ai == 'set_bugowner':
+ args = opts.actiondata[i]
+ actionsxml += self._set_bugowner(args, opts)
+ i = i+1
+ else:
+ raise oscerr.WrongArgs('Unsupported action %s' % ai)
+ if actionsxml == "":
+ sys.exit('No actions need to be taken.')
+
+ if not opts.message:
+ opts.message = edit_message()
+
+ import cgi
+ xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \
+ (actionsxml, cgi.escape(opts.message or ""))
+ u = makeurl(apiurl, ['request'], query='cmd=create')
+ f = http_POST(u, data=xml)
+
+ root = ET.parse(f).getroot()
+ rid = root.get('id')
+ for srid in supersede:
+ change_request_state(apiurl, srid, 'superseded',
+ 'superseded by %s' % rid, rid)
+ return rid
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-r', '--role', metavar='role',
+ help='specify user role (default: maintainer)')
+ @cmdln.alias("reqbugownership")
+ @cmdln.alias("requestbugownership")
+ @cmdln.alias("reqmaintainership")
+ @cmdln.alias("reqms")
+ @cmdln.alias("reqbs")
+ def do_requestmaintainership(self, subcmd, opts, *args):
+ """${cmd_name}: requests to add user as maintainer or bugowner
+
+ usage:
+ osc requestmaintainership # for current user in checked out package
+ osc requestmaintainership USER # for specified user in checked out package
+ osc requestmaintainership PROJECT # for current user if cwd is not a checked out package
+ osc requestmaintainership PROJECT PACKAGE # for current user
+ osc requestmaintainership PROJECT PACKAGE USER # request for specified user
+ osc requestmaintainership PROJECT PACKAGE group:NAME # request for specified group
+
+ osc requestbugownership ... # accepts same parameters but uses bugowner role
+
+ ${cmd_option_list}
+ """
+ import cgi
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ if len(args) == 2:
+ project = args[0]
+ package = args[1]
+ user = conf.get_apiurl_usr(apiurl)
+ elif len(args) == 3:
+ project = args[0]
+ package = args[1]
+ user = args[2]
+ elif len(args) < 2 and is_package_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ if len(args) == 0:
+ user = conf.get_apiurl_usr(apiurl)
+ else:
+ user = args[0]
+ elif len(args) == 1:
+ user = conf.get_apiurl_usr(apiurl)
+ project = args[0]
+ package = None
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ role = 'maintainer'
+ if subcmd in ( 'reqbugownership', 'requestbugownership', 'reqbs' ):
+ role = 'bugowner'
+ if opts.role:
+ role = opts.role
+ if not role in ('maintainer', 'bugowner'):
+ raise oscerr.WrongOptions('invalid \'--role\': either specify \'maintainer\' or \'bugowner\'')
+ if not opts.message:
+ opts.message = edit_message()
+
+ r = Request()
+ if user.startswith('group:'):
+ group = user.replace('group:', '')
+ if role == 'bugowner':
+ r.add_action('set_bugowner', tgt_project=project, tgt_package=package,
+ group_name=group)
+ else:
+ r.add_action('add_role', tgt_project=project, tgt_package=package,
+ group_name=group, group_role=role)
+ elif role == 'bugowner':
+ r.add_action('set_bugowner', tgt_project=project, tgt_package=package,
+ person_name=user)
+ else:
+ r.add_action('add_role', tgt_project=project, tgt_package=package,
+ person_name=user, person_role=role)
+ r.description = cgi.escape(opts.message or '')
+ r.create(apiurl)
+ print(r.reqid)
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-r', '--repository', metavar='REPOSITORY',
+ help='specify repository')
+ @cmdln.option('--accept-in-hours', metavar='HOURS',
+ help='specify time when request shall get accepted automatically. Only works with write permissions in target.')
+ @cmdln.alias("dr")
+ @cmdln.alias("dropreq")
+ @cmdln.alias("droprequest")
+ @cmdln.alias("deletereq")
+ def do_deleterequest(self, subcmd, opts, *args):
+ """${cmd_name}: Request to delete (or 'drop') a package or project
+
+ usage:
+ osc deletereq [-m TEXT] # works in checked out project/package
+ osc deletereq [-m TEXT] PROJECT [PACKAGE]
+ osc deletereq [-m TEXT] PROJECT [--repository REPOSITORY]
+ ${cmd_option_list}
+ """
+ import cgi
+
+ args = slash_split(args)
+
+ project = None
+ package = None
+ repository = None
+
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+ elif len(args) == 1:
+ project = args[0]
+ elif len(args) == 2:
+ project = args[0]
+ package = args[1]
+ elif is_project_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ elif is_package_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ else:
+ raise oscerr.WrongArgs('Please specify at least a project.')
+
+ if opts.repository:
+ repository = opts.repository
+
+ if not opts.message:
+ import textwrap
+ if package is not None:
+ footer = textwrap.TextWrapper(width = 66).fill(
+ 'please explain why you like to delete package %s of project %s'
+ % (package, project))
+ else:
+ footer = textwrap.TextWrapper(width = 66).fill(
+ 'please explain why you like to delete project %s' % project)
+ opts.message = edit_message(footer)
+
+ r = Request()
+ r.add_action('delete', tgt_project=project, tgt_package=package, tgt_repository=repository)
+ r.description = cgi.escape(opts.message)
+ if opts.accept_in_hours:
+ r.accept_at_in_hours(int(opts.accept_in_hours))
+ r.create(self.get_api_url())
+ print(r.reqid)
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.alias("cr")
+ @cmdln.alias("changedevelreq")
+ def do_changedevelrequest(self, subcmd, opts, *args):
+ """${cmd_name}: Create request to change the devel package definition.
+
+ [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
+ for information on this topic.]
+
+ See the "request" command for showing and modifing existing requests.
+
+ osc changedevelrequest PROJECT PACKAGE DEVEL_PROJECT [DEVEL_PACKAGE]
+ """
+ import cgi
+
+ if len(args) == 0 and is_package_dir('.') and find_default_project():
+ wd = os.curdir
+ devel_project = store_read_project(wd)
+ devel_package = package = store_read_package(wd)
+ project = find_default_project(self.get_api_url(), package)
+ elif len(args) < 3:
+ raise oscerr.WrongArgs('Too few arguments.')
+ elif len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+ else:
+ devel_project = args[2]
+ project = args[0]
+ package = args[1]
+ devel_package = package
+ if len(args) == 4:
+ devel_package = args[3]
+
+ if not opts.message:
+ import textwrap
+ footer = textwrap.TextWrapper(width = 66).fill(
+ 'please explain why you like to change the devel project of %s/%s to %s/%s'
+ % (project, package, devel_project, devel_package))
+ opts.message = edit_message(footer)
+
+ r = Request()
+ r.add_action('change_devel', src_project=devel_project, src_package=devel_package,
+ tgt_project=project, tgt_package=package)
+ r.description = cgi.escape(opts.message)
+ r.create(self.get_api_url())
+ print(r.reqid)
+
+
+ @cmdln.option('-d', '--diff', action='store_true',
+ help='generate a diff')
+ @cmdln.option('-u', '--unified', action='store_true',
+ help='output the diff in the unified diff format')
+ @cmdln.option('--no-devel', action='store_true',
+ help='Do not attempt to forward to devel project')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-t', '--type', metavar='TYPE',
+ help='limit to requests which contain a given action type (submit/delete/change_devel)')
+ @cmdln.option('-a', '--all', action='store_true',
+ help='all states. Same as\'-s all\'')
+ @cmdln.option('-f', '--force', action='store_true',
+ help='enforce state change, can be used to ignore open reviews')
+ @cmdln.option('-s', '--state', default='', # default is 'all' if no args given, 'declined,new,review' otherwise
+ help='only list requests in one of the comma separated given states (new/review/accepted/revoked/declined) or "all" [default="declined,new,review", or "all", if no args given]')
+ @cmdln.option('-D', '--days', metavar='DAYS',
+ help='only list requests in state "new" or changed in the last DAYS. [default=%(request_list_days)s]')
+ @cmdln.option('-U', '--user', metavar='USER',
+ help='requests or reviews limited for the specified USER')
+ @cmdln.option('-G', '--group', metavar='GROUP',
+ help='requests or reviews limited for the specified GROUP')
+ @cmdln.option('-P', '--project', metavar='PROJECT',
+ help='requests or reviews limited for the specified PROJECT')
+ @cmdln.option('-p', '--package', metavar='PACKAGE',
+ help='requests or reviews limited for the specified PACKAGE, requires also a PROJECT')
+ @cmdln.option('-b', '--brief', action='store_true', default=False,
+ help='print output in list view as list subcommand')
+ @cmdln.option('-M', '--mine', action='store_true',
+ help='only show requests created by yourself')
+ @cmdln.option('-B', '--bugowner', action='store_true',
+ help='also show requests about packages where I am bugowner')
+ @cmdln.option('-e', '--edit', action='store_true',
+ help='edit a submit action')
+ @cmdln.option('-i', '--interactive', action='store_true',
+ help='interactive review of request')
+ @cmdln.option('--or-revoke', action='store_true',
+ help='For automatisation scripts: accepts (if using with accept argument) a request when it is in new or review state. Or revoke it when it got declined. Otherwise just do nothing.')
+ @cmdln.option('--non-interactive', action='store_true',
+ help='non-interactive review of request')
+ @cmdln.option('--exclude-target-project', action='append',
+ help='exclude target project from request list')
+ @cmdln.option('--involved-projects', action='store_true',
+ help='show all requests for project/packages where USER is involved')
+ @cmdln.option('--source-buildstatus', action='store_true',
+ help='print the buildstatus of the source package (only works with "show" and the interactive review)')
+ @cmdln.alias("rq")
+ @cmdln.alias("review")
+ # FIXME: rewrite this mess and split request and review
+ def do_request(self, subcmd, opts, *args):
+ """${cmd_name}: Show or modify requests and reviews
+
+ [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
+ for information on this topic.]
+
+ The 'request' command has the following sub commands:
+
+ "list" lists open requests attached to a project or package or person.
+ Uses the project/package of the current directory if none of
+ -M, -U USER, project/package are given.
+
+ "log" will show the history of the given ID
+
+ "show" will show the request itself, and generate a diff for review, if
+ used with the --diff option. The keyword show can be omitted if the ID is numeric.
+
+ "decline" will change the request state to "declined"
+
+ "reopen" will set the request back to new or review.
+
+ "setincident" will direct "maintenance" requests into specific incidents
+
+ "supersede" will supersede one request with another existing one.
+
+ "revoke" will set the request state to "revoked"
+
+ "accept" will change the request state to "accepted" and will trigger
+ the actual submit process. That would normally be a server-side copy of
+ the source package to the target package.
+
+ "checkout" will checkout the request's source package ("submit" requests only).
+
+ "priorize" change the prioritity of a request to either "critical", "important", "moderate" or "low"
+
+
+ The 'review' command has the following sub commands:
+
+ "list" lists open requests that need to be reviewed by the
+ specified user or group
+
+ "add" adds a person or group as reviewer to a request
+
+ "accept" mark the review positive
+
+ "decline" mark the review negative. A negative review will
+ decline the request.
+
+ usage:
+ osc request list [-M] [-U USER] [-s state] [-D DAYS] [-t type] [-B] [PRJ [PKG]]
+ osc request log ID
+ osc request [show] [-d] [-b] ID
+
+ osc request accept [-m TEXT] ID
+ osc request decline [-m TEXT] ID
+ osc request revoke [-m TEXT] ID
+ osc request reopen [-m TEXT] ID
+ osc request setincident [-m TEXT] ID INCIDENT
+ osc request supersede [-m TEXT] ID SUPERSEDING_ID
+ osc request approvenew [-m TEXT] PROJECT
+ osc request priorize [-m TEXT] ID PRIORITY
+
+ osc request checkout/co ID
+ osc request clone [-m TEXT] ID
+
+ osc review show [-d] [-b] ID
+ osc review list [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] [-s state]
+ osc review add [-m TEXT] [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] ID
+ osc review accept [-m TEXT] [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] ID
+ osc review decline [-m TEXT] [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] ID
+ osc review reopen [-m TEXT] [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] ID
+ osc review supersede [-m TEXT] [-U USER] [-G GROUP] [-P PROJECT [-p PACKAGE]] ID SUPERSEDING_ID
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ if opts.all and opts.state:
+ raise oscerr.WrongOptions('Sorry, the options \'--all\' and \'--state\' ' \
+ 'are mutually exclusive.')
+ if opts.mine and opts.user:
+ raise oscerr.WrongOptions('Sorry, the options \'--user\' and \'--mine\' ' \
+ 'are mutually exclusive.')
+ if opts.interactive and opts.non_interactive:
+ raise oscerr.WrongOptions('Sorry, the options \'--interactive\' and ' \
+ '\'--non-interactive\' are mutually exclusive')
+
+ if not args:
+ args = [ 'list' ]
+ opts.mine = 1
+ if opts.state == '':
+ opts.state = 'all'
+
+ if opts.state == '' and subcmd != 'review':
+ opts.state = 'declined,new,review'
+
+ if args[0] == 'help':
+ return self.do_help(['help', 'request'])
+
+ cmds = ['list', 'ls', 'log', 'show', 'decline', 'reopen', 'clone', 'accept', 'approvenew', 'wipe', 'setincident', 'supersede', 'revoke', 'checkout', 'co', 'priorize']
+ if subcmd != 'review' and args[0] not in cmds:
+ raise oscerr.WrongArgs('Unknown request action %s. Choose one of %s.' \
+ % (args[0], ', '.join(cmds)))
+ cmds = ['show', 'list', 'add', 'decline', 'accept', 'reopen', 'supersede']
+ if subcmd == 'review' and args[0] not in cmds:
+ raise oscerr.WrongArgs('Unknown review action %s. Choose one of %s.' \
+ % (args[0], ', '.join(cmds)))
+
+ cmd = args[0]
+ del args[0]
+ if cmd == 'ls':
+ cmd = "list"
+
+ apiurl = self.get_api_url()
+
+ if cmd in ['list']:
+ min_args, max_args = 0, 2
+ elif cmd in ['supersede', 'setincident', 'priorize']:
+ min_args, max_args = 2, 2
+ else:
+ min_args, max_args = 1, 1
+ if len(args) < min_args:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) > max_args:
+ raise oscerr.WrongArgs('Too many arguments.')
+ if cmd in ['add'] and not opts.user and not opts.group and not opts.project:
+ raise oscerr.WrongArgs('No reviewer specified.')
+
+ source_buildstatus = conf.config['request_show_source_buildstatus'] or opts.source_buildstatus
+
+ reqid = None
+ supersedid = None
+ if cmd == 'list' or cmd == 'approvenew':
+ package = None
+ project = None
+ if len(args) > 0:
+ project = args[0]
+ elif not opts.mine and not opts.user:
+ try:
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ except oscerr.NoWorkingCopy:
+ pass
+ elif opts.project:
+ project = opts.project
+ if opts.package:
+ package = opts.package
+
+ if len(args) > 1:
+ package = args[1]
+ elif cmd == 'supersede':
+ reqid = args[0]
+ supersedid = args[1]
+ elif cmd == 'setincident':
+ reqid = args[0]
+ incident = args[1]
+ elif cmd == 'priorize':
+ reqid = args[0]
+ priority = args[1]
+ elif cmd in ['log', 'add', 'show', 'decline', 'reopen', 'clone', 'accept', 'wipe', 'revoke', 'checkout', 'co']:
+ reqid = args[0]
+
+ # clone all packages from a given request
+ if cmd in ['clone']:
+ # should we force a message?
+ print('Cloned packages are available in project: %s' % clone_request(apiurl, reqid, opts.message))
+
+ # change incidents
+ elif cmd == 'setincident':
+ query = { 'cmd': 'setincident', 'incident': incident }
+ url = makeurl(apiurl, ['request', reqid], query)
+ r = http_POST(url, data=opts.message)
+ print(ET.parse(r).getroot().get('code'))
+
+ # change priority
+ elif cmd == 'priorize':
+ query = { 'cmd': 'setpriority', 'priority': priority }
+ url = makeurl(apiurl, ['request', reqid], query)
+ r = http_POST(url, data=opts.message)
+ print(ET.parse(r).getroot().get('code'))
+
+ # add new reviewer to existing request
+ elif cmd in ['add'] and subcmd == 'review':
+ query = { 'cmd': 'addreview' }
+ if opts.user:
+ query['by_user'] = opts.user
+ if opts.group:
+ query['by_group'] = opts.group
+ if opts.project:
+ query['by_project'] = opts.project
+ if opts.package:
+ query['by_package'] = opts.package
+ url = makeurl(apiurl, ['request', reqid], query)
+ if not opts.message:
+ opts.message = edit_message()
+ r = http_POST(url, data=opts.message)
+ print(ET.parse(r).getroot().get('code'))
+
+ # list and approvenew
+ elif cmd == 'list' or cmd == 'approvenew':
+ states = ('new', 'accepted', 'revoked', 'declined', 'review', 'superseded')
+ who = ''
+ if cmd == 'approvenew':
+ states = ('new')
+ results = get_request_list(apiurl, project, package, '', ['new'])
+ else:
+ state_list = opts.state.split(',')
+ if state_list == ['']:
+ state_list = ()
+ if opts.all:
+ state_list = ['all']
+ else:
+ for s in state_list:
+ if not s in states and not s == 'all':
+ raise oscerr.WrongArgs('Unknown state \'%s\', try one of %s' % (s, ','.join(states)))
+ if opts.mine:
+ who = conf.get_apiurl_usr(apiurl)
+ if opts.user:
+ who = opts.user
+
+ ## FIXME -B not implemented!
+ if opts.bugowner:
+ if (self.options.debug):
+ print('list: option --bugowner ignored: not impl.')
+
+ if subcmd == 'review':
+ # FIXME: do the review list for the user and for all groups he belong to
+ results = get_review_list(apiurl, project, package, who, opts.group, opts.project, opts.package, state_list)
+ else:
+ if opts.involved_projects:
+ who = who or conf.get_apiurl_usr(apiurl)
+ results = get_user_projpkgs_request_list(apiurl, who, req_state=state_list,
+ req_type=opts.type, exclude_projects=opts.exclude_target_project or [])
+ else:
+ results = get_request_list(apiurl, project, package, who,
+ state_list, opts.type, opts.exclude_target_project or [])
+
+ # Check if project actually exists if result list is empty
+ if not results:
+ if project:
+ msg = 'No results for %(kind)s %(entity)s'
+ emsg = '%(kind)s %(entity)s does not exist'
+ d = {'entity': [project], 'kind': 'project'}
+ meth = show_project_meta
+ if package:
+ d['kind'] = 'package'
+ d['entity'].append(package)
+ meth = show_package_meta
+ try:
+ entity = d['entity']
+ d['entity'] = '/'.join(entity)
+ meth(apiurl, *entity)
+ print(msg % d)
+ except HTTPError:
+ print(emsg % d)
+ else:
+ print('No results')
+ return
+
+ # we must not sort the results here, since the api is doing it already "the right way"
+ days = opts.days or conf.config['request_list_days']
+ since = ''
+ try:
+ days = float(days)
+ except ValueError:
+ days = 0
+ if days > 0:
+ since = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(time.time()-days*24*3600))
+
+ skipped = 0
+ ## bs has received 2009-09-20 a new xquery compare() function
+ ## which allows us to limit the list inside of get_request_list
+ ## That would be much faster for coolo. But counting the remainder
+ ## would not be possible with current xquery implementation.
+ ## Workaround: fetch all, and filter on client side.
+
+ ## FIXME: date filtering should become implemented on server side
+ for result in results:
+ if days == 0 or result.state.when > since or result.state.name == 'new':
+ if (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive:
+ ignore_reviews = subcmd != 'review'
+ request_interactive_review(apiurl, result, group=opts.group,
+ ignore_reviews=ignore_reviews,
+ source_buildstatus=source_buildstatus)
+ else:
+ print(result.list_view(), '\n')
+ else:
+ skipped += 1
+ if skipped:
+ print("There are %d requests older than %s days.\n" % (skipped, days))
+
+ if cmd == 'approvenew':
+ print("\n *** Approve them all ? [y/n] ***")
+ if sys.stdin.read(1) == "y":
+
+ if not opts.message:
+ opts.message = edit_message()
+ for result in results:
+ print(result.reqid, ": ", end=' ')
+ r = change_request_state(apiurl,
+ result.reqid, 'accepted', opts.message or '', force=opts.force)
+ print('Result of change request state: %s' % r)
+ else:
+ print('Aborted...', file=sys.stderr)
+ raise oscerr.UserAbort()
+
+ elif cmd == 'log':
+ for l in get_request_log(apiurl, reqid):
+ print(l)
+
+ # show
+ elif cmd == 'show':
+ r = get_request(apiurl, reqid)
+ if opts.brief:
+ print(r.list_view())
+ elif opts.edit:
+ if not r.get_actions('submit'):
+ raise oscerr.WrongOptions('\'--edit\' not possible ' \
+ '(request has no \'submit\' action)')
+ return request_interactive_review(apiurl, r, 'e')
+ elif (opts.interactive or conf.config['request_show_interactive']) and not opts.non_interactive:
+ ignore_reviews = subcmd != 'review'
+ return request_interactive_review(apiurl, r, group=opts.group,
+ ignore_reviews=ignore_reviews,
+ source_buildstatus=source_buildstatus)
+ else:
+ print(r)
+ print_comments(apiurl, 'request', reqid)
+ if source_buildstatus:
+ sr_actions = r.get_actions('submit')
+ if not sr_actions:
+ raise oscerr.WrongOptions( '\'--source-buildstatus\' not possible ' \
+ '(request has no \'submit\' actions)')
+ for action in sr_actions:
+ print('Buildstatus for \'%s/%s\':' % (action.src_project, action.src_package))
+ print('\n'.join(get_results(apiurl, action.src_project, action.src_package)))
+ if opts.diff:
+ diff = ''
+ try:
+ # works since OBS 2.1
+ diff = request_diff(apiurl, reqid)
+ except HTTPError as e:
+ # for OBS 2.0 and before
+ sr_actions = r.get_actions('submit')
+ if not r.get_actions('submit') and not r.get_actions('maintenance_incident') and not r.get_actions('maintenance_release'):
+ raise oscerr.WrongOptions('\'--diff\' not possible (request has no supported actions)')
+ for action in sr_actions:
+ diff += 'old: %s/%s\nnew: %s/%s\n' % (action.src_project, action.src_package,
+ action.tgt_project, action.tgt_package)
+ diff += submit_action_diff(apiurl, action)
+ diff += '\n\n'
+ run_pager(diff, tmp_suffix='')
+
+ # checkout
+ elif cmd == 'checkout' or cmd == 'co':
+ r = get_request(apiurl, reqid)
+ sr_actions = r.get_actions('submit', 'maintenance_release')
+ if not sr_actions:
+ raise oscerr.WrongArgs('\'checkout\' not possible (request has no \'submit\' actions)')
+ for action in sr_actions:
+ checkout_package(apiurl, action.src_project, action.src_package, \
+ action.src_rev, expand_link=True, prj_dir=action.src_project)
+
+ else:
+ state_map = {'reopen' : 'new', 'accept' : 'accepted', 'decline' : 'declined', 'wipe' : 'deleted', 'revoke' : 'revoked', 'supersede' : 'superseded'}
+ # Change review state only
+ if subcmd == 'review':
+ if not opts.message:
+ opts.message = edit_message()
+ if cmd in ['accept', 'decline', 'reopen', 'supersede']:
+ if opts.user or opts.group or opts.project or opts.package:
+ r = change_review_state(apiurl, reqid, state_map[cmd], opts.user, opts.group, opts.project,
+ opts.package, opts.message or '', supersed=supersedid)
+ print(r)
+ else:
+ rq = get_request(apiurl, reqid)
+ if rq.state.name in ['new', 'review']:
+ for review in rq.reviews: # try all, but do not fail on error
+ try:
+ r = change_review_state(apiurl, reqid, state_map[cmd], review.by_user, review.by_group,
+ review.by_project, review.by_package, opts.message or '', supersed=supersedid)
+ print(r)
+ except HTTPError as e:
+ body = e.read()
+ if e.code in [403]:
+ if review.by_user:
+ print('No permission on review by user %s:' % review.by_user)
+ if review.by_group:
+ print('No permission on review by group %s' % review.by_group)
+ if review.by_package:
+ print('No permission on review by package %s / %s' % (review.by_project, review.by_package))
+ elif review.by_project:
+ print('No permission on review by project %s' % review.by_project)
+ print(e, file=sys.stderr)
+ else:
+ print('Request is closed, please reopen the request first before changing any reviews.')
+ # Change state of entire request
+ elif cmd in ['reopen', 'accept', 'decline', 'wipe', 'revoke', 'supersede']:
+ rq = get_request(apiurl, reqid)
+ if opts.or_revoke:
+ if rq.state.name == "declined":
+ cmd = "revoke"
+ elif rq.state.name != "new" and rq.state.name != "review":
+ return 0
+ if rq.state.name == state_map[cmd]:
+ repl = raw_input("\n *** The state of the request (#%s) is already '%s'. Change state anyway? [y/n] *** " % \
+ (reqid, rq.state.name))
+ if repl.lower() != 'y':
+ print('Aborted...', file=sys.stderr)
+ raise oscerr.UserAbort()
+
+ if not opts.message:
+ tmpl = change_request_state_template(rq, state_map[cmd])
+ opts.message = edit_message(template=tmpl)
+ try:
+ r = change_request_state(apiurl,
+ reqid, state_map[cmd], opts.message or '', supersed=supersedid, force=opts.force)
+ print('Result of change request state: %s' % r)
+ except HTTPError as e:
+ print(e, file=sys.stderr)
+ details = e.hdrs.get('X-Opensuse-Errorcode')
+ if details:
+ print(details, file=sys.stderr)
+ root = ET.fromstring(e.read())
+ summary = root.find('summary')
+ if not summary is None:
+ print(summary.text)
+ if opts.or_revoke:
+ if e.code in [ 400, 403, 404, 500 ]:
+ print('Revoking it ...')
+ r = change_request_state(apiurl,
+ reqid, 'revoked', opts.message or '', supersed=supersedid, force=opts.force)
+ sys.exit(1)
+
+
+ # check for devel instances after accepted requests
+ if cmd in ['accept']:
+ import cgi
+ sr_actions = rq.get_actions('submit')
+ for action in sr_actions:
+ u = makeurl(apiurl, ['/search/package'], {
+ 'match' : "([devel/[@project='%s' and @package='%s']])" % (action.tgt_project, action.tgt_package)
+ })
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ if root.findall('package') and not opts.no_devel:
+ for node in root.findall('package'):
+ project = node.get('project')
+ package = node.get('name')
+ # skip it when this is anyway a link to me
+ link_url = makeurl(apiurl, ['source', project, package])
+ links_to_project = links_to_package = None
+ try:
+ file = http_GET(link_url)
+ root = ET.parse(file).getroot()
+ link_node = root.find('linkinfo')
+ if link_node != None:
+ links_to_project = link_node.get('project') or project
+ links_to_package = link_node.get('package') or package
+ except HTTPError as e:
+ if e.code != 404:
+ print('Cannot get list of files for %s/%s: %s' % (project, package, e), file=sys.stderr)
+ except SyntaxError as e:
+ print('Cannot parse list of files for %s/%s: %s' % (project, package, e), file=sys.stderr)
+ if links_to_project == action.tgt_project and links_to_package == action.tgt_package:
+ # links to my request target anyway, no need to forward submit
+ continue
+
+ print(project, end=' ')
+ if package != action.tgt_package:
+ print("/", package, end=' ')
+ repl = raw_input('\nForward this submit to it? ([y]/n)')
+ if repl.lower() == 'y' or repl == '':
+ (supersede, reqs) = check_existing_requests(apiurl, action.tgt_project, action.tgt_package,
+ project, package)
+ msg = "%s (forwarded request %s from %s)" % (rq.description, reqid, rq.get_creator())
+ rid = create_submit_request(apiurl, action.tgt_project, action.tgt_package,
+ project, package, cgi.escape(msg))
+ print(msg)
+ print("New request #", rid)
+ for req in reqs:
+ change_request_state(apiurl, req.reqid, 'superseded',
+ 'superseded by %s' % rid, rid)
+
+ # editmeta and its aliases are all depracated
+ @cmdln.alias("editprj")
+ @cmdln.alias("createprj")
+ @cmdln.alias("editpac")
+ @cmdln.alias("createpac")
+ @cmdln.alias("edituser")
+ @cmdln.alias("usermeta")
+ @cmdln.hide(1)
+ def do_editmeta(self, subcmd, opts, *args):
+ """${cmd_name}:
+
+ Obsolete command to edit metadata. Use 'meta' now.
+
+ See the help output of 'meta'.
+
+ """
+
+ print('This command is obsolete. Use \'osc meta <metatype> ...\'.', file=sys.stderr)
+ print('See \'osc help meta\'.', file=sys.stderr)
+ #self.do_help([None, 'meta'])
+ return 2
+
+
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='use the specified revision.')
+ @cmdln.option('-R', '--use-plain-revision', action='store_true',
+ help='Do not expand revision the specified or latest rev')
+ @cmdln.option('-u', '--unset', action='store_true',
+ help='remove revision in link, it will point always to latest revision')
+ def do_setlinkrev(self, subcmd, opts, *args):
+ """${cmd_name}: Updates a revision number in a source link.
+
+ This command adds or updates a specified revision number in a source link.
+ The current revision of the source is used, if no revision number is specified.
+
+ usage:
+ osc setlinkrev
+ osc setlinkrev PROJECT [PACKAGE]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ package = None
+ rev = parseRevisionOption(opts.revision)[0] or ''
+ if opts.unset:
+ rev = None
+
+ if len(args) == 0:
+ p = findpacs(os.curdir)[0]
+ project = p.prjname
+ package = p.name
+ apiurl = p.apiurl
+ if not p.islink():
+ sys.exit('Local directory is no checked out source link package, aborting')
+ elif len(args) == 2:
+ project = args[0]
+ package = args[1]
+ elif len(args) == 1:
+ project = args[0]
+ else:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('setlinkrev'))
+
+ if package:
+ packages = [package]
+ else:
+ packages = meta_get_packagelist(apiurl, project)
+
+ for p in packages:
+ rev = set_link_rev(apiurl, project, p, revision=rev,
+ expand=not opts.use_plain_revision)
+ if rev is None:
+ print('removed revision from link')
+ else:
+ print('set revision to %s for package %s' % (rev, p))
+
+
+ def do_linktobranch(self, subcmd, opts, *args):
+ """${cmd_name}: Convert a package containing a classic link with patch to a branch
+
+ This command tells the server to convert a _link with or without a project.diff
+ to a branch. This is a full copy with a _link file pointing to the branched place.
+
+ usage:
+ osc linktobranch # can be used in checked out package
+ osc linktobranch PROJECT PACKAGE
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ if len(args) == 0:
+ wd = os.curdir
+ project = store_read_project(wd)
+ package = store_read_package(wd)
+ update_local_dir = True
+ elif len(args) < 2:
+ raise oscerr.WrongArgs('Too few arguments (required none or two)')
+ elif len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments (required none or two)')
+ else:
+ project = args[0]
+ package = args[1]
+ update_local_dir = False
+
+ # execute
+ link_to_branch(apiurl, project, package)
+ if update_local_dir:
+ pac = Package(wd)
+ pac.update(rev=pac.latest_rev())
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ def do_detachbranch(self, subcmd, opts, *args):
+ """${cmd_name}: replace a link with its expanded sources
+
+ If a package is a link it is replaced with its expanded sources. The link
+ does not exist anymore.
+
+ usage:
+ osc detachbranch # can be used in package working copy
+ osc detachbranch PROJECT PACKAGE
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ if len(args) == 0:
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ elif len(args) == 2:
+ project, package = args
+ elif len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments (required none or two)')
+ else:
+ raise oscerr.WrongArgs('Too few arguments (required none or two)')
+
+ try:
+ copy_pac(apiurl, project, package, apiurl, project, package, expand=True, comment=opts.message)
+ except HTTPError as e:
+ root = ET.fromstring(show_files_meta(apiurl, project, package, 'latest', expand=False))
+ li = Linkinfo()
+ li.read(root.find('linkinfo'))
+ if li.islink() and li.haserror():
+ raise oscerr.LinkExpandError(project, package, li.error)
+ elif not li.islink():
+ print('package \'%s/%s\' is no link' % (project, package), file=sys.stderr)
+ else:
+ raise e
+
+
+ @cmdln.option('-C', '--cicount', choices=['add', 'copy', 'local'],
+ help='cicount attribute in the link, known values are add, copy, and local, default in buildservice is currently add.')
+ @cmdln.option('-c', '--current', action='store_true',
+ help='link fixed against current revision.')
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='link the specified revision.')
+ @cmdln.option('-f', '--force', action='store_true',
+ help='overwrite an existing link file if it is there.')
+ @cmdln.option('-d', '--disable-publish', action='store_true',
+ help='disable publishing of the linked package')
+ @cmdln.option('-N', '--new-package', action='store_true',
+ help='create a link to a not yet existing package')
+ def do_linkpac(self, subcmd, opts, *args):
+ """${cmd_name}: "Link" a package to another package
+
+ A linked package is a clone of another package, but plus local
+ modifications. It can be cross-project.
+
+ The DESTPAC name is optional; the source packages' name will be used if
+ DESTPAC is omitted.
+
+ Afterwards, you will want to 'checkout DESTPRJ DESTPAC'.
+
+ To add a patch, add the patch as file and add it to the _link file.
+ You can also specify text which will be inserted at the top of the spec file.
+
+ See the examples in the _link file.
+
+ NOTE: In case you want to fix or update another package, you should use the 'branch'
+ command. A branch has correct repositories (and a link) setup up by default and
+ will be cleaned up automatically after it was submitted back.
+
+ usage:
+ osc linkpac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ if not args or len(args) < 3:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('linkpac'))
+
+ rev, dummy = parseRevisionOption(opts.revision)
+ vrev = None
+
+ src_project = args[0]
+ src_package = args[1]
+ dst_project = args[2]
+ if len(args) > 3:
+ dst_package = args[3]
+ else:
+ dst_package = src_package
+
+ if src_project == dst_project and src_package == dst_package:
+ raise oscerr.WrongArgs('Error: source and destination are the same.')
+
+ if src_project == dst_project and not opts.cicount:
+ # in this case, the user usually wants to build different spec
+ # files from the same source
+ opts.cicount = "copy"
+
+ if opts.current and not opts.new_package:
+ rev, vrev = show_upstream_rev_vrev(apiurl, src_project, src_package, expand=True)
+ if rev == None or len(rev) < 32:
+ # vrev is only needed for srcmd5 and OBS instances < 2.1.17 do not support it
+ vrev = None
+
+ if rev and not checkRevision(src_project, src_package, rev):
+ print('Revision \'%s\' does not exist' % rev, file=sys.stderr)
+ sys.exit(1)
+
+ link_pac(src_project, src_package, dst_project, dst_package, opts.force, rev, opts.cicount, opts.disable_publish, opts.new_package, vrev)
+
+ @cmdln.option('--nosources', action='store_true',
+ help='ignore source packages when copying build results to destination project')
+ @cmdln.option('-m', '--map-repo', metavar='SRC=TARGET[,SRC=TARGET]',
+ help='Allows repository mapping(s) to be given as SRC=TARGET[,SRC=TARGET]')
+ @cmdln.option('-d', '--disable-publish', action='store_true',
+ help='disable publishing of the aggregated package')
+ def do_aggregatepac(self, subcmd, opts, *args):
+ """${cmd_name}: "Aggregate" a package to another package
+
+ Aggregation of a package means that the build results (binaries) of a
+ package are basically copied into another project.
+ This can be used to make packages available from building that are
+ needed in a project but available only in a different project. Note
+ that this is done at the expense of disk space. See
+ http://en.opensuse.org/openSUSE:Build_Service_Tips_and_Tricks#link_and_aggregate
+ for more information.
+
+ The DESTPAC name is optional; the source packages' name will be used if
+ DESTPAC is omitted.
+
+ usage:
+ osc aggregatepac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ if not args or len(args) < 3:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('aggregatepac'))
+
+ src_project = args[0]
+ src_package = args[1]
+ dst_project = args[2]
+ if len(args) > 3:
+ dst_package = args[3]
+ else:
+ dst_package = src_package
+
+ if src_project == dst_project and src_package == dst_package:
+ raise oscerr.WrongArgs('Error: source and destination are the same.')
+
+ repo_map = {}
+ if opts.map_repo:
+ for pair in opts.map_repo.split(','):
+ src_tgt = pair.split('=')
+ if len(src_tgt) != 2:
+ raise oscerr.WrongOptions('map "%s" must be SRC=TARGET[,SRC=TARGET]' % opts.map_repo)
+ repo_map[src_tgt[0]] = src_tgt[1]
+
+ aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map, opts.disable_publish, opts.nosources)
+
+
+ @cmdln.option('-c', '--client-side-copy', action='store_true',
+ help='do a (slower) client-side copy')
+ @cmdln.option('-k', '--keep-maintainers', action='store_true',
+ help='keep original maintainers. Default is remove all and replace with the one calling the script.')
+ @cmdln.option('-K', '--keep-link', action='store_true',
+ help='keep the source link in target, this also expands the source')
+ @cmdln.option('-d', '--keep-develproject', action='store_true',
+ help='keep develproject tag in the package metadata')
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='copy the specified revision.')
+ @cmdln.option('-t', '--to-apiurl', metavar='URL',
+ help='URL of destination api server. Default is the source api server.')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-e', '--expand', action='store_true',
+ help='if the source package is a link then copy the expanded version of the link')
+ def do_copypac(self, subcmd, opts, *args):
+ """${cmd_name}: Copy a package
+
+ A way to copy package to somewhere else.
+
+ It can be done across buildservice instances, if the -t option is used.
+ In that case, a client-side copy and link expansion are implied.
+
+ Using --client-side-copy always involves downloading all files, and
+ uploading them to the target.
+
+ The DESTPAC name is optional; the source packages' name will be used if
+ DESTPAC is omitted.
+
+ usage:
+ osc copypac SOURCEPRJ SOURCEPAC DESTPRJ [DESTPAC]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ if not args or len(args) < 3:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('copypac'))
+
+ src_project = args[0]
+ src_package = args[1]
+ dst_project = args[2]
+ if len(args) > 3:
+ dst_package = args[3]
+ else:
+ dst_package = src_package
+
+ src_apiurl = conf.config['apiurl']
+ if opts.to_apiurl:
+ dst_apiurl = conf.config['apiurl_aliases'].get(opts.to_apiurl, opts.to_apiurl)
+ else:
+ dst_apiurl = src_apiurl
+
+ if src_apiurl != dst_apiurl:
+ opts.client_side_copy = True
+ opts.expand = True
+
+ rev, dummy = parseRevisionOption(opts.revision)
+
+ if opts.message:
+ comment = opts.message
+ else:
+ if not rev:
+ rev = show_upstream_rev(src_apiurl, src_project, src_package)
+ comment = 'osc copypac from project:%s package:%s revision:%s' % ( src_project, src_package, rev )
+ if opts.keep_link:
+ comment += ", using keep-link"
+ if opts.expand:
+ comment += ", using expand"
+ if opts.client_side_copy:
+ comment += ", using client side copy"
+
+ if src_project == dst_project and \
+ src_package == dst_package and \
+ not rev and \
+ src_apiurl == dst_apiurl:
+ raise oscerr.WrongArgs('Source and destination are the same.')
+
+ r = copy_pac(src_apiurl, src_project, src_package,
+ dst_apiurl, dst_project, dst_package,
+ client_side_copy=opts.client_side_copy,
+ keep_maintainers=opts.keep_maintainers,
+ keep_develproject=opts.keep_develproject,
+ expand=opts.expand,
+ revision=rev,
+ comment=comment,
+ keep_link=opts.keep_link)
+ print(r)
+
+
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='Release only binaries from the specified repository')
+ @cmdln.option('--target-project', metavar='TARGETPROJECT',
+ help='Release only to specified project')
+ @cmdln.option('--target-repository', metavar='TARGETREPOSITORY',
+ help='Release only to specified repository')
+ @cmdln.option('--set-release', metavar='RELEASETAG',
+ help='rename binaries during release using this release tag')
+ def do_release(self, subcmd, opts, *args):
+ """${cmd_name}: Release sources and binaries
+
+ This command is used to transfer sources and binaries without rebuilding them.
+ It requires defined release targets set to trigger="manual". Please refer the
+ release management chapter in the OBS book for details.
+
+ usage:
+ osc release [ SOURCEPROJECT [ SOURCEPACKAGE ] ]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ source_project = source_package = None
+
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 0:
+ if is_project_dir(os.curdir):
+ source_project = store_read_project(os.curdir)
+ elif is_package_dir(os.curdir):
+ source_package = store_read_package(wd)
+ else:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) > 0:
+ source_project = args[0]
+ if len(args) > 1:
+ source_package = args[1]
+
+ query = { 'cmd': 'release' }
+ if opts.target_project:
+ query["target_project"] = opts.target_project
+ if opts.target_repository:
+ query["target_repository"] = opts.target_repository
+ if opts.repo:
+ query["repository"] = opts.repo
+ if opts.set_release:
+ query["setrelease"] = opts.set_release
+ baseurl = ['source', source_project]
+ if source_package:
+ baseurl.append(source_package)
+ url = makeurl(apiurl, baseurl, query=query)
+ f = http_POST(url)
+ while True:
+ buf = f.read(16384)
+ if not buf:
+ break
+ sys.stdout.write(buf)
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ def do_releaserequest(self, subcmd, opts, *args):
+ """${cmd_name}: Create a request for releasing a maintenance update.
+
+ [See http://doc.opensuse.org/products/draft/OBS/obs-reference-guide_draft/cha.obs.maintenance_setup.html
+ for information on this topic.]
+
+ This command is used by the maintence team to start the release process of a maintenance update.
+ This includes usually testing based on the defined reviewers of the update project.
+
+ usage:
+ osc releaserequest [ SOURCEPROJECT ]
+
+ ${cmd_option_list}
+ """
+
+ # FIXME: additional parameters can be a certain repo list to create a partitial release
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ source_project = None
+
+ if len(args) > 1:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 0 and is_project_dir(os.curdir):
+ source_project = store_read_project(os.curdir)
+ elif len(args) == 0:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) > 0:
+ source_project = args[0]
+
+ if not opts.message:
+ opts.message = edit_message()
+
+ r = create_release_request(apiurl, source_project, opts.message)
+ print(r.reqid)
+
+
+
+ @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
+ help='Use this attribute to find default maintenance project (default is OBS:MaintenanceProject)')
+ @cmdln.option('--noaccess', action='store_true',
+ help='Create a hidden project')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ def do_createincident(self, subcmd, opts, *args):
+ """${cmd_name}: Create a maintenance incident
+
+ [See http://doc.opensuse.org/products/draft/OBS/obs-reference-guide_draft/cha.obs.maintenance_setup.html
+ for information on this topic.]
+
+ This command is asking to open an empty maintence incident. This can usually only be done by a responsible
+ maintenance team.
+ Please see the "mbranch" command on how to full such a project content and
+ the "patchinfo" command how add the required maintenance update information.
+
+ usage:
+ osc createincident [ MAINTENANCEPROJECT ]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ maintenance_attribute = conf.config['maintenance_attribute']
+ if opts.attribute:
+ maintenance_attribute = opts.attribute
+
+ source_project = target_project = None
+
+ if len(args) > 1:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 1:
+ target_project = args[0]
+ else:
+ xpath = 'attribute/@name = \'%s\'' % maintenance_attribute
+ res = search(apiurl, project_id=xpath)
+ root = res['project_id']
+ project = root.find('project')
+ if project is None:
+ sys.exit('Unable to find defined OBS:MaintenanceProject project on server.')
+ target_project = project.get('name')
+ print('Using target project \'%s\'' % target_project)
+
+ query = { 'cmd': 'createmaintenanceincident' }
+ if opts.noaccess:
+ query["noaccess"] = 1
+ url = makeurl(apiurl, ['source', target_project], query=query)
+ r = http_POST(url, data=opts.message)
+ project = None
+ for i in ET.fromstring(r.read()).findall('data'):
+ if i.get('name') == 'targetproject':
+ project = i.text.strip()
+ if project:
+ print("Incident project created: ", project)
+ else:
+ print(ET.parse(r).getroot().get('code'))
+ print(ET.parse(r).getroot().get('error'))
+
+
+ @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
+ help='Use this attribute to find default maintenance project (default is OBS:MaintenanceProject)')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('--release-project', metavar='RELEASEPROJECT',
+ help='Specify the release project')
+ @cmdln.option('--no-cleanup', action='store_true',
+ help='do not remove source project on accept')
+ @cmdln.option('--cleanup', action='store_true',
+ help='do remove source project on accept')
+ @cmdln.option('--incident', metavar='INCIDENT',
+ help='specify incident number to merge in')
+ @cmdln.option('--incident-project', metavar='INCIDENT_PROJECT',
+ help='specify incident project to merge in')
+ @cmdln.option('-s', '--supersede', metavar='SUPERSEDE',
+ help='Superseding another request by this one')
+ @cmdln.alias("mr")
+ def do_maintenancerequest(self, subcmd, opts, *args):
+ """${cmd_name}: Create a request for starting a maintenance incident.
+
+ [See http://doc.opensuse.org/products/draft/OBS/obs-reference-guide_draft/cha.obs.maintenance_setup.html
+ for information on this topic.]
+
+ This command is asking the maintence team to start a maintence incident based on a
+ created maintenance update. Please see the "mbranch" command on how to create such a project and
+ the "patchinfo" command how add the required maintenance update information.
+
+ usage:
+ osc maintenancerequest [ SOURCEPROJECT [ SOURCEPACKAGES RELEASEPROJECT ] ]
+ osc maintenancerequest .
+
+ The 2nd line when issued within a package directory provides a short cut to submit a single
+ package (the one in the current directory) from the project of this package to be submitted
+ to the release project this package links to. This syntax is only valid when specified from
+ a package subdirectory.
+ ${cmd_option_list}
+ """
+ #FIXME: the follow syntax would make more sense and would obsolete the --release-project parameter
+ # but is incompatible with the current one
+ # osc maintenancerequest [ SOURCEPROJECT [ RELEASEPROJECT [ SOURCEPACKAGES ] ]
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ maintenance_attribute = conf.config['maintenance_attribute']
+ if opts.attribute:
+ maintenance_attribute = opts.attribute
+
+ source_project = target_project = release_project = opt_sourceupdate = None
+ source_packages = []
+
+ if len(args) == 0 and (is_project_dir(os.curdir) or is_package_dir(os.curdir)):
+ source_project = store_read_project(os.curdir)
+ elif len(args) == 0:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) > 0:
+ if len(args) == 1 and args[0] == '.':
+ if is_package_dir(os.curdir):
+ source_project = store_read_project(os.curdir)
+ source_packages = [store_read_package(os.curdir)]
+ p = Package(os.curdir)
+ release_project = p.linkinfo.project
+ else:
+ raise oscerr.WrongArgs('No package directory')
+ else:
+ source_project = args[0]
+ if len(args) > 1:
+ if len(args) == 2:
+ sys.exit('Source package defined, but no release project.')
+ source_packages = args[1:]
+ release_project = args[-1]
+ source_packages.remove(release_project)
+ if opts.cleanup:
+ opt_sourceupdate = 'cleanup'
+ if not opts.no_cleanup:
+ default_branch = 'home:%s:branches:' % (conf.get_apiurl_usr(apiurl))
+ if source_project.startswith(default_branch):
+ opt_sourceupdate = 'cleanup'
+
+ if opts.release_project:
+ release_project = opts.release_project
+
+ if opts.incident_project:
+ target_project = opts.incident_project
+ else:
+ xpath = 'attribute/@name = \'%s\'' % maintenance_attribute
+ res = search(apiurl, project_id=xpath)
+ root = res['project_id']
+ project = root.find('project')
+ if project is None:
+ sys.exit('Unable to find defined OBS:MaintenanceProject project on server.')
+ target_project = project.get('name')
+ if opts.incident:
+ target_project += ":" + opts.incident
+ print('Using target project \'%s\'' % target_project)
+
+ if not opts.message:
+ opts.message = edit_message()
+
+ supersede_existing = False
+ reqs = []
+ if not opts.supersede:
+ (supersede_existing, reqs) = check_existing_maintenance_requests(apiurl,
+ source_project,
+ source_packages,
+ target_project,
+ None) # unspecified release project
+
+ r = create_maintenance_request(apiurl, source_project, source_packages, target_project, release_project, opt_sourceupdate, opts.message)
+ print(r.reqid)
+
+ if supersede_existing:
+ for req in reqs:
+ change_request_state(apiurl, req.reqid, 'superseded',
+ 'superseded by %s' % r.reqid, r.reqid)
+
+ if opts.supersede:
+ change_request_state(apiurl, opts.supersede, 'superseded',
+ opts.message or '', r.reqid)
+
+
+ @cmdln.option('-c', '--checkout', action='store_true',
+ help='Checkout branched package afterwards ' \
+ '(\'osc bco\' is a shorthand for this option)' )
+ @cmdln.option('-a', '--attribute', metavar='ATTRIBUTE',
+ help='Use this attribute to find affected packages (default is OBS:Maintained)')
+ @cmdln.option('-u', '--update-project-attribute', metavar='UPDATE_ATTRIBUTE',
+ help='Use this attribute to find update projects (default is OBS:UpdateProject) ')
+ @cmdln.option('--dryrun', action='store_true',
+ help='Just simulate the action and report back the result.')
+ @cmdln.option('--noaccess', action='store_true',
+ help='Create a hidden project')
+ @cmdln.option('--nodevelproject', action='store_true',
+ help='do not follow a defined devel project ' \
+ '(primary project where a package is developed)')
+ @cmdln.alias('sm')
+ @cmdln.alias('maintained')
+ def do_mbranch(self, subcmd, opts, *args):
+ """${cmd_name}: Search or branch multiple instances of a package
+
+ This command is used for searching all relevant instances of packages
+ and creating links of them in one project.
+ This is esp. used for maintenance updates. It can also be used to branch
+ all packages marked before with a given attribute.
+
+ [See http://en.opensuse.org/openSUSE:Build_Service_Concept_Maintenance
+ for information on this topic.]
+
+ The branched package will live in
+ home:USERNAME:branches:ATTRIBUTE:PACKAGE
+ if nothing else specified.
+
+ usage:
+ osc sm [SOURCEPACKAGE] [-a ATTRIBUTE]
+ osc mbranch [ SOURCEPACKAGE [ TARGETPROJECT ] ]
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ tproject = None
+
+ maintained_attribute = conf.config['maintained_attribute']
+ if opts.attribute:
+ maintained_attribute = opts.attribute
+ maintained_update_project_attribute = conf.config['maintained_update_project_attribute']
+ if opts.update_project_attribute:
+ maintained_update_project_attribute = opts.update_project_attribute
+
+ if not len(args) or len(args) > 2:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+ if len(args) >= 1:
+ package = args[0]
+ if len(args) >= 2:
+ tproject = args[1]
+
+ if subcmd == 'sm' or subcmd == 'maintained':
+ opts.dryrun = 1
+
+ result = attribute_branch_pkg(apiurl, maintained_attribute, maintained_update_project_attribute, \
+ package, tproject, noaccess = opts.noaccess, nodevelproject=opts.nodevelproject, dryrun=opts.dryrun)
+
+ if result is None:
+ print('ERROR: Attribute branch call came not back with a project.', file=sys.stderr)
+ sys.exit(1)
+
+ if opts.dryrun:
+ for r in result.findall('package'):
+ line="%s/%s"%(r.get('project'), r.get('package'))
+ for d in r.findall('devel'):
+ line+=" using sources from %s/%s"%(d.get('project'), d.get('package'))
+ print(line)
+ return
+
+ apiopt = ''
+ if conf.get_configParser().get('general', 'apiurl') != apiurl:
+ apiopt = '-A %s ' % apiurl
+ print('A working copy of the maintenance branch can be checked out with:\n\n' \
+ 'osc %sco %s' \
+ % (apiopt, result))
+
+ if opts.checkout:
+ Project.init_project(apiurl, result, result, conf.config['do_package_tracking'])
+ print(statfrmt('A', result))
+
+ # all packages
+ for package in meta_get_packagelist(apiurl, result):
+ try:
+ checkout_package(apiurl, result, package, expand_link = True, prj_dir = result)
+ except:
+ print('Error while checkout package:\n', package, file=sys.stderr)
+
+ if conf.config['verbose']:
+ print('Note: You can use "osc delete" or "osc submitpac" when done.\n')
+
+
+ @cmdln.alias('branchco')
+ @cmdln.alias('bco')
+ @cmdln.alias('getpac')
+ @cmdln.option('--nodevelproject', action='store_true',
+ help='do not follow a defined devel project ' \
+ '(primary project where a package is developed)')
+ @cmdln.option('-c', '--checkout', action='store_true',
+ help='Checkout branched package afterwards using "co -e -S"' \
+ '(\'osc bco\' is a shorthand for this option)' )
+ @cmdln.option('-f', '--force', default=False, action="store_true",
+ help='force branch, overwrite target')
+ @cmdln.option('--add-repositories', default=False, action="store_true",
+ help='Add repositories to target project (happens by default when project is new)')
+ @cmdln.option('--extend-package-names', default=False, action="store_true",
+ help='Extend packages names with project name as suffix')
+ @cmdln.option('--noaccess', action='store_true',
+ help='Create a hidden project')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify message TEXT')
+ @cmdln.option('-M', '--maintenance', default=False, action="store_true",
+ help='Create project and package in maintenance mode')
+ @cmdln.option('-N', '--new-package', action='store_true',
+ help='create a branch pointing to a not yet existing package')
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='branch against a specific revision')
+ @cmdln.option('--linkrev', metavar='linkrev',
+ help='specify the used revision in the link target.')
+ @cmdln.option('--add-repositories-block', metavar='add_repositories_block',
+ help='specify the used block strategy for new repositories')
+ @cmdln.option('--add-repositories-rebuild', metavar='add_repositories_rebuild',
+ help='specify the used rebuild strategy for new repositories')
+ def do_branch(self, subcmd, opts, *args):
+ """${cmd_name}: Branch a package
+
+ [See http://en.opensuse.org/openSUSE:Build_Service_Collaboration
+ for information on this topic.]
+
+ Create a source link from a package of an existing project to a new
+ subproject of the requesters home project (home:branches:)
+
+ The branched package will live in
+ home:USERNAME:branches:PROJECT/PACKAGE
+ if nothing else specified.
+
+ With getpac or bco, the branched package will come from one of
+ %(getpac_default_project)s
+ (list of projects from oscrc:getpac_default_project)
+ if nothing else is specfied on the command line.
+
+ In case of branch errors, where the source has currently merge
+ conflicts use --linkrev=base option.
+
+ usage:
+ osc branch
+ osc branch SOURCEPROJECT SOURCEPACKAGE
+ osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT
+ osc branch SOURCEPROJECT SOURCEPACKAGE TARGETPROJECT TARGETPACKAGE
+ osc getpac SOURCEPACKAGE
+ osc bco ...
+ ${cmd_option_list}
+ """
+
+ if subcmd == 'getpac' or subcmd == 'branchco' or subcmd == 'bco':
+ opts.checkout = True
+ args = slash_split(args)
+ tproject = tpackage = None
+
+ if (subcmd == 'getpac' or subcmd == 'bco') and len(args) == 1:
+ def_p = find_default_project(self.get_api_url(), args[0])
+ print('defaulting to %s/%s' % (def_p, args[0]), file=sys.stderr)
+ # python has no args.unshift ???
+ args = [ def_p, args[0] ]
+
+ if len(args) == 0 and is_package_dir('.'):
+ args = (store_read_project('.'), store_read_package('.'))
+
+ if len(args) < 2 or len(args) > 4:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ apiurl = self.get_api_url()
+
+ expected = 'home:%s:branches:%s' % (conf.get_apiurl_usr(apiurl), args[0])
+ if len(args) >= 3:
+ expected = tproject = args[2]
+ if len(args) >= 4:
+ tpackage = args[3]
+
+ try:
+ exists, targetprj, targetpkg, srcprj, srcpkg = \
+ branch_pkg(apiurl, args[0], args[1],
+ nodevelproject=opts.nodevelproject, rev=opts.revision,
+ linkrev=opts.linkrev,
+ target_project=tproject, target_package=tpackage,
+ return_existing=opts.checkout, msg=opts.message or '',
+ force=opts.force, noaccess=opts.noaccess,
+ add_repositories=opts.add_repositories,
+ add_repositories_block=opts.add_repositories_block,
+ add_repositories_rebuild=opts.add_repositories_rebuild,
+ extend_package_names=opts.extend_package_names,
+ missingok=opts.new_package,
+ maintenance=opts.maintenance)
+ except oscerr.NotMissing as e:
+ print('NOTE: Package target exists already via project links, link will point to given project.')
+ print(' A submission will initialize a new instance.')
+ exists, targetprj, targetpkg, srcprj, srcpkg = \
+ branch_pkg(apiurl, args[0], args[1],
+ nodevelproject=opts.nodevelproject, rev=opts.revision,
+ linkrev=opts.linkrev,
+ target_project=tproject, target_package=tpackage,
+ return_existing=opts.checkout, msg=opts.message or '',
+ force=opts.force, noaccess=opts.noaccess,
+ add_repositories=opts.add_repositories,
+ add_repositories_block=opts.add_repositories_block,
+ add_repositories_rebuild=opts.add_repositories_rebuild,
+ extend_package_names=opts.extend_package_names,
+ missingok=False,
+ maintenance=opts.maintenance,
+ newinstance=opts.new_package)
+
+ if exists:
+ print('Using existing branch project: %s' % targetprj, file=sys.stderr)
+
+ devloc = None
+ if not exists and (srcprj != args[0] or srcpkg != args[1]):
+ try:
+ root = ET.fromstring(''.join(show_attribute_meta(apiurl, args[0], None, None,
+ conf.config['maintained_update_project_attribute'], False, False)))
+ # this might raise an AttributeError
+ uproject = root.find('attribute').find('value').text
+ print('\nNote: The branch has been created from the configured update project: %s' \
+ % uproject)
+ except (AttributeError, HTTPError) as e:
+ devloc = srcprj
+ print('\nNote: The branch has been created of a different project,\n' \
+ ' %s,\n' \
+ ' which is the primary location of where development for\n' \
+ ' that package takes place.\n' \
+ ' That\'s also where you would normally make changes against.\n' \
+ ' A direct branch of the specified package can be forced\n' \
+ ' with the --nodevelproject option.\n' % devloc)
+
+ package = targetpkg or args[1]
+ if opts.checkout:
+ checkout_package(apiurl, targetprj, package, server_service_files=False,
+ expand_link=True, prj_dir=targetprj)
+ if conf.config['verbose']:
+ print('Note: You can use "osc delete" or "osc submitpac" when done.\n')
+ else:
+ apiopt = ''
+ if conf.get_configParser().get('general', 'apiurl') != apiurl:
+ apiopt = '-A %s ' % apiurl
+ print('A working copy of the branched package can be checked out with:\n\n' \
+ 'osc %sco %s/%s' \
+ % (apiopt, targetprj, package))
+ print_request_list(apiurl, args[0], args[1])
+ if devloc:
+ print_request_list(apiurl, devloc, srcpkg)
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify log message TEXT')
+ def do_undelete(self, subcmd, opts, *args):
+ """${cmd_name}: Restores a deleted project or package on the server.
+
+ The server restores a package including the sources and meta configuration.
+ Binaries remain to be lost and will be rebuild.
+
+ usage:
+ osc undelete PROJECT
+ osc undelete PROJECT PACKAGE [PACKAGE ...]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ if len(args) < 1:
+ raise oscerr.WrongArgs('Missing argument.')
+
+ msg = ''
+ if opts.message:
+ msg = opts.message
+ else:
+ msg = edit_message()
+
+ apiurl = self.get_api_url()
+ prj = args[0]
+ pkgs = args[1:]
+
+ if pkgs:
+ for pkg in pkgs:
+ undelete_package(apiurl, prj, pkg, msg)
+ else:
+ undelete_project(apiurl, prj, msg)
+
+
+ @cmdln.option('-r', '--recursive', action='store_true',
+ help='deletes a project with packages inside')
+ @cmdln.option('-f', '--force', action='store_true',
+ help='deletes a project where other depends on')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify log message TEXT')
+ def do_rdelete(self, subcmd, opts, *args):
+ """${cmd_name}: Delete a project or packages on the server.
+
+ As a safety measure, project must be empty (i.e., you need to delete all
+ packages first). Also, packages must have no requests pending (i.e., you need
+ to accept/revoke such requests first).
+ If you are sure that you want to remove this project and all
+ its packages use \'--recursive\' switch.
+ It may still not work because other depends on it. If you want to ignore this as
+ well use \'--force\' switch.
+
+ usage:
+ osc rdelete [-r] [-f] PROJECT [PACKAGE]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ if len(args) < 1 or len(args) > 2:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ apiurl = self.get_api_url()
+ prj = args[0]
+
+ msg = ''
+ if opts.message:
+ msg = opts.message
+ else:
+ msg = edit_message()
+
+ # empty arguments result in recursive project delete ...
+ if not len(prj):
+ raise oscerr.WrongArgs('Project argument is empty')
+
+ if len(args) > 1:
+ pkg = args[1]
+
+ if not len(pkg):
+ raise oscerr.WrongArgs('Package argument is empty')
+
+ ## FIXME: core.py:commitDelPackage() should have something similar
+ rlist = get_request_list(apiurl, prj, pkg)
+ for rq in rlist:
+ print(rq)
+ if len(rlist) >= 1 and not opts.force:
+ print('Package has pending requests. Deleting the package will break them. '\
+ 'They should be accepted/declined/revoked before deleting the package. '\
+ 'Or just use \'--force\'.', file=sys.stderr)
+ sys.exit(1)
+
+ delete_package(apiurl, prj, pkg, opts.force, msg)
+
+ elif (not opts.recursive) and len(meta_get_packagelist(apiurl, prj)) >= 1:
+ print('Project contains packages. It must be empty before deleting it. ' \
+ 'If you are sure that you want to remove this project and all its ' \
+ 'packages use the \'--recursive\' switch.', file=sys.stderr)
+ sys.exit(1)
+ else:
+ delete_project(apiurl, prj, opts.force, msg)
+
+
+ def do_lock(self, subcmd, opts, project, package=None):
+ """${cmd_name}: Locks a project or package.
+
+ usage:
+ osc lock PROJECT [PACKAGE]
+
+ ${cmd_option_list}
+ """
+ apiurl = self.get_api_url()
+ kind = 'prj'
+ path_args = (project,)
+ if package is not None:
+ kind = 'pkg'
+ path_args = (project, package)
+ meta = meta_exists(kind, path_args, create_new=False, apiurl=apiurl)
+ root = ET.fromstring(''.join(meta))
+ if root.find('lock') is not None:
+ print('Already locked', file=sys.stderr)
+ sys.exit(1)
+ # alternatively, we could also use the set_flag api call
+ # instead of manually manipulating the xml
+ lock = ET.SubElement(root, 'lock')
+ ET.SubElement(lock, 'enable')
+ meta = ET.tostring(root)
+ edit_meta(kind, path_args=path_args, data=meta)
+
+
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify log message TEXT')
+ def do_unlock(self, subcmd, opts, *args):
+ """${cmd_name}: Unlocks a project or package
+
+ Unlocks a locked project or package. A comment is required.
+
+ usage:
+ osc unlock PROJECT [PACKAGE]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ if len(args) < 1 or len(args) > 2:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ apiurl = self.get_api_url()
+ prj = args[0]
+
+ msg = ''
+ if opts.message:
+ msg = opts.message
+ else:
+ msg = edit_message()
+
+ # empty arguments result in recursive project delete ...
+ if not len(prj):
+ raise oscerr.WrongArgs('Project argument is empty')
+
+ if len(args) > 1:
+ pkg = args[1]
+
+ if not len(pkg):
+ raise oscerr.WrongArgs('Package argument is empty')
+
+ unlock_package(apiurl, prj, pkg, msg)
+
+ else:
+ unlock_project(apiurl, prj, msg)
+
+
+ @cmdln.hide(1)
+ def do_deletepac(self, subcmd, opts, *args):
+ print("""${cmd_name} is obsolete !
+
+ Please use either
+ osc delete for checked out packages or projects
+ or
+ osc rdelete for server side operations.""")
+
+ sys.exit(1)
+
+ @cmdln.hide(1)
+ @cmdln.option('-f', '--force', action='store_true',
+ help='deletes a project and its packages')
+ def do_deleteprj(self, subcmd, opts, project):
+ """${cmd_name} is obsolete !
+
+ Please use
+ osc rdelete PROJECT
+ """
+ sys.exit(1)
+
+ @cmdln.alias('metafromspec')
+ @cmdln.alias('updatepkgmetafromspec')
+ @cmdln.option('', '--specfile', metavar='FILE',
+ help='Path to specfile. (if you pass more than working copy this option is ignored)')
+ def do_updatepacmetafromspec(self, subcmd, opts, *args):
+ """${cmd_name}: Update package meta information from a specfile
+
+ ARG, if specified, is a package working copy.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ args = parseargs(args)
+ if opts.specfile and len(args) == 1:
+ specfile = opts.specfile
+ else:
+ specfile = None
+ pacs = findpacs(args)
+ for p in pacs:
+ p.read_meta_from_spec(specfile)
+ p.update_package_meta()
+
+
+ @cmdln.alias('linkdiff')
+ @cmdln.alias('ldiff')
+ @cmdln.alias('di')
+ @cmdln.option('-c', '--change', metavar='rev',
+ help='the change made by revision rev (like -r rev-1:rev).'
+ 'If rev is negative this is like -r rev:rev-1.')
+ @cmdln.option('-r', '--revision', metavar='rev1[:rev2]',
+ help='If rev1 is specified it will compare your working copy against '
+ 'the revision (rev1) on the server. '
+ 'If rev1 and rev2 are specified it will compare rev1 against rev2 '
+ '(NOTE: changes in your working copy are ignored in this case)')
+ @cmdln.option('-p', '--plain', action='store_true',
+ help='output the diff in plain (not unified) diff format')
+ @cmdln.option('-l', '--link', action='store_true',
+ help='(osc linkdiff): compare against the base revision of the link')
+ @cmdln.option('--missingok', action='store_true',
+ help='do not fail if the source or target project/package does not exist on the server')
+ def do_diff(self, subcmd, opts, *args):
+ """${cmd_name}: Generates a diff
+
+ Generates a diff, comparing local changes against the repository
+ server.
+
+ ${cmd_usage}
+ ARG, if specified, is a filename to include in the diff.
+ Default: all files.
+
+ osc diff --link
+ osc linkdiff
+ Compare current checkout directory against the link base.
+
+ osc diff --link PROJ PACK
+ osc linkdiff PROJ PACK
+ Compare a package against the link base (ignoring working copy changes).
+
+ ${cmd_option_list}
+ """
+
+ if (subcmd == 'ldiff' or subcmd == 'linkdiff'):
+ opts.link = True
+ args = parseargs(args)
+
+ pacs = None
+ if not opts.link or not len(args) == 2:
+ pacs = findpacs(args)
+
+
+ if opts.link:
+ query = { 'rev': 'latest' }
+ if pacs:
+ u = makeurl(pacs[0].apiurl, ['source', pacs[0].prjname, pacs[0].name], query=query)
+ else:
+ u = makeurl(self.get_api_url(), ['source', args[0], args[1]], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ raise oscerr.APIError('package is not a source link')
+ baserev = linkinfo.get('baserev')
+ opts.revision = baserev
+ if pacs:
+ print("diff working copy against last commited version\n")
+ else:
+ print("diff commited package against linked revision %s\n" % baserev)
+ run_pager(server_diff(self.get_api_url(), linkinfo.get('project'), linkinfo.get('package'), baserev,
+ args[0], args[1], linkinfo.get('lsrcmd5'), not opts.plain, opts.missingok))
+ return
+
+ if opts.change:
+ try:
+ rev = int(opts.change)
+ if rev > 0:
+ rev1 = rev - 1
+ rev2 = rev
+ elif rev < 0:
+ rev1 = -rev
+ rev2 = -rev - 1
+ else:
+ return
+ except:
+ print('Revision \'%s\' not an integer' % opts.change, file=sys.stderr)
+ return
+ else:
+ rev1, rev2 = parseRevisionOption(opts.revision)
+ diff = ''
+ for pac in pacs:
+ if not rev2:
+ for i in pac.get_diff(rev1):
+ diff += ''.join(i)
+ else:
+ diff += server_diff_noex(pac.apiurl, pac.prjname, pac.name, rev1,
+ pac.prjname, pac.name, rev2, not opts.plain, opts.missingok)
+ run_pager(diff)
+
+
+ @cmdln.option('--oldprj', metavar='OLDPRJ',
+ help='project to compare against'
+ ' (deprecated, use 3 argument form)')
+ @cmdln.option('--oldpkg', metavar='OLDPKG',
+ help='package to compare against'
+ ' (deprecated, use 3 argument form)')
+ @cmdln.option('-M', '--meta', action='store_true',
+ help='diff meta data')
+ @cmdln.option('-r', '--revision', metavar='N[:M]',
+ help='revision id, where N = old revision and M = new revision')
+ @cmdln.option('-p', '--plain', action='store_true',
+ help='output the diff in plain (not unified) diff format')
+ @cmdln.option('-c', '--change', metavar='rev',
+ help='the change made by revision rev (like -r rev-1:rev). '
+ 'If rev is negative this is like -r rev:rev-1.')
+ @cmdln.option('--missingok', action='store_true',
+ help='do not fail if the source or target project/package does not exist on the server')
+ @cmdln.option('-u', '--unexpand', action='store_true',
+ help='diff unexpanded version if sources are linked')
+ def do_rdiff(self, subcmd, opts, *args):
+ """${cmd_name}: Server-side "pretty" diff of two packages
+
+ Compares two packages (three or four arguments) or shows the
+ changes of a specified revision of a package (two arguments)
+
+ If no revision is specified the latest revision is used.
+
+ Note that this command doesn't return a normal diff (which could be
+ applied as patch), but a "pretty" diff, which also compares the content
+ of tarballs.
+
+
+ usage:
+ osc ${cmd_name} OLDPRJ OLDPAC NEWPRJ [NEWPAC]
+ osc ${cmd_name} PROJECT PACKAGE
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ rev1 = None
+ rev2 = None
+
+ old_project = None
+ old_package = None
+ new_project = None
+ new_package = None
+
+ if len(args) == 2:
+ new_project = args[0]
+ new_package = args[1]
+ if opts.oldprj:
+ old_project = opts.oldprj
+ if opts.oldpkg:
+ old_package = opts.oldpkg
+ elif len(args) == 3 or len(args) == 4:
+ if opts.oldprj or opts.oldpkg:
+ raise oscerr.WrongArgs('--oldpkg and --oldprj are only valid with two arguments')
+ old_project = args[0]
+ new_package = old_package = args[1]
+ new_project = args[2]
+ if len(args) == 4:
+ new_package = args[3]
+ elif len(args) == 1 and opts.meta:
+ new_project = args[0]
+ new_package = '_project'
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ if opts.meta:
+ opts.unexpand = True
+
+ if opts.change:
+ try:
+ rev = int(opts.change)
+ if rev > 0:
+ rev1 = rev - 1
+ rev2 = rev
+ elif rev < 0:
+ rev1 = -rev
+ rev2 = -rev - 1
+ else:
+ return
+ except:
+ print('Revision \'%s\' not an integer' % opts.change, file=sys.stderr)
+ return
+ else:
+ if opts.revision:
+ rev1, rev2 = parseRevisionOption(opts.revision)
+
+ rdiff = server_diff_noex(apiurl,
+ old_project, old_package, rev1,
+ new_project, new_package, rev2, not opts.plain, opts.missingok,
+ meta=opts.meta,
+ expand=not opts.unexpand)
+
+ run_pager(rdiff)
+
+ def _pdiff_raise_non_existing_package(self, project, package, msg = None):
+ raise oscerr.PackageMissing(project, package, msg or '%s/%s does not exist.' % (project, package))
+
+ def _pdiff_package_exists(self, apiurl, project, package):
+ try:
+ show_package_meta(apiurl, project, package)
+ return True
+ except HTTPError as e:
+ if e.code != 404:
+ print('Cannot check that %s/%s exists: %s' % (project, package, e), file=sys.stderr)
+ return False
+
+ def _pdiff_guess_parent(self, apiurl, project, package, check_exists_first = False):
+ # Make sure the parent exists
+ if check_exists_first and not self._pdiff_package_exists(apiurl, project, package):
+ self._pdiff_raise_non_existing_package(project, package)
+
+ if project.startswith('home:'):
+ guess = project[len('home:'):]
+ # remove user name
+ pos = guess.find(':')
+ if pos > 0:
+ guess = guess[guess.find(':') + 1:]
+ if guess.startswith('branches:'):
+ guess = guess[len('branches:'):]
+ return (guess, package)
+
+ return (None, None)
+
+ def _pdiff_get_parent_from_link(self, apiurl, project, package):
+ link_url = makeurl(apiurl, ['source', project, package, '_link'])
+
+ try:
+ file = http_GET(link_url)
+ root = ET.parse(file).getroot()
+ except HTTPError as e:
+ return (None, None)
+ except SyntaxError as e:
+ print('Cannot parse %s/%s/_link: %s' % (project, package, e), file=sys.stderr)
+ return (None, None)
+
+ parent_project = root.get('project')
+ parent_package = root.get('package') or package
+
+ if parent_project is None:
+ return (None, None)
+
+ return (parent_project, parent_package)
+
+ def _pdiff_get_exists_and_parent(self, apiurl, project, package):
+ link_url = makeurl(apiurl, ['public', 'source', project, package])
+ try:
+ file = http_GET(link_url)
+ root = ET.parse(file).getroot()
+ except HTTPError as e:
+ if e.code != 404:
+ print('Cannot get list of files for %s/%s: %s' % (project, package, e), file=sys.stderr)
+ return (None, None, None)
+ except SyntaxError as e:
+ print('Cannot parse list of files for %s/%s: %s' % (project, package, e), file=sys.stderr)
+ return (None, None, None)
+
+ link_node = root.find('linkinfo')
+ if link_node is None:
+ return (True, None, None)
+
+ parent_project = link_node.get('project')
+ parent_package = link_node.get('package') or package
+
+ if parent_project is None:
+ raise oscerr.APIError('%s/%s is a link with no parent?' % (project, package))
+
+ return (True, parent_project, parent_package)
+
+ @cmdln.option('-p', '--plain', action='store_true',
+ dest='plain',
+ help='output the diff in plain (not unified) diff format')
+ @cmdln.option('-n', '--nomissingok', action='store_true',
+ dest='nomissingok',
+ help='fail if the parent package does not exist on the server')
+ def do_pdiff(self, subcmd, opts, *args):
+ """${cmd_name}: Quick alias to diff the content of a package with its parent.
+
+ Usage:
+ osc pdiff [--plain|-p] [--nomissing-ok|-n]
+ osc pdiff [--plain|-p] [--nomissing-ok|-n] PKG
+ osc pdiff [--plain|-p] [--nomissing-ok|-n] PRJ PKG
+
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ args = slash_split(args)
+
+ unified = not opts.plain
+ noparentok = not opts.nomissingok
+
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 0:
+ if not is_package_dir(os.getcwd()):
+ raise oscerr.WrongArgs('Current directory is not a checked out package. Please specify a project and a package.')
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ elif len(args) == 1:
+ if not is_project_dir(os.getcwd()):
+ raise oscerr.WrongArgs('Current directory is not a checked out project. Please specify a project and a package.')
+ project = store_read_project(os.curdir)
+ package = args[0]
+ elif len(args) == 2:
+ project = args[0]
+ package = args[1]
+ else:
+ raise RuntimeError('Internal error: bad check for arguments.')
+
+ ## Find parent package
+
+ # Old way, that does one more request to api
+ #(parent_project, parent_package) = self._pdiff_get_parent_from_link(apiurl, project, package)
+ #if not parent_project:
+ # (parent_project, parent_package) = self._pdiff_guess_parent(apiurl, project, package, check_exists_first = True)
+ # if parent_project and parent_package:
+ # print 'Guessed that %s/%s is the parent package.' % (parent_project, parent_package)
+
+ # New way
+ (exists, parent_project, parent_package) = self._pdiff_get_exists_and_parent (apiurl, project, package)
+ if not exists:
+ self._pdiff_raise_non_existing_package(project, package)
+ if not parent_project:
+ (parent_project, parent_package) = self._pdiff_guess_parent(apiurl, project, package, check_exists_first = False)
+ if parent_project and parent_package:
+ print('Guessed that %s/%s is the parent package.' % (parent_project, parent_package))
+
+ if not parent_project or not parent_package:
+ print('Cannot find a parent for %s/%s to diff against.' % (project, package), file=sys.stderr)
+ return 1
+
+ if not noparentok and not self._pdiff_package_exists(apiurl, parent_project, parent_package):
+ self._pdiff_raise_non_existing_package(parent_project, parent_package,
+ msg = 'Parent for %s/%s (%s/%s) does not exist.' % \
+ (project, package, parent_project, parent_package))
+
+ rdiff = server_diff(apiurl, parent_project, parent_package, None, project,
+ package, None, unified = unified, missingok = noparentok)
+
+ run_pager(rdiff)
+
+ def _get_branch_parent(self, prj):
+ m = re.match('^home:[^:]+:branches:(.+)', prj)
+ # OBS_Maintained is a special case
+ if m and prj.find(':branches:OBS_Maintained:') == -1:
+ return m.group(1)
+ return None
+
+ def _prdiff_skip_package(self, opts, pkg):
+ if opts.exclude and re.search(opts.exclude, pkg):
+ return True
+
+ if opts.include and not re.search(opts.include, pkg):
+ return True
+
+ return False
+
+ def _prdiff_output_diff(self, opts, rdiff):
+ if opts.diffstat:
+ print()
+ p = subprocess.Popen("diffstat",
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ close_fds=True)
+ p.stdin.write(rdiff.encode())
+ p.stdin.close()
+ print("".join(x.decode() for x in p.stdout.readlines()))
+ elif opts.unified:
+ print()
+ print(rdiff)
+ #run_pager(rdiff)
+
+ def _prdiff_output_matching_requests(self, opts, requests,
+ srcprj, pkg):
+ """
+ Search through the given list of requests and output any
+ submitrequests which target pkg and originate from srcprj.
+ """
+ for req in requests:
+ for action in req.get_actions('submit'):
+ if action.src_project != srcprj:
+ continue
+
+ if action.tgt_package != pkg:
+ continue
+
+ print()
+ print(req.list_view())
+ break
+
+ @cmdln.alias('projectdiff')
+ @cmdln.alias('projdiff')
+ @cmdln.option('-r', '--requests', action='store_true',
+ help='show open requests for any packages with differences')
+ @cmdln.option('-e', '--exclude', metavar='REGEXP', dest='exclude',
+ help='skip packages matching REGEXP')
+ @cmdln.option('-i', '--include', metavar='REGEXP', dest='include',
+ help='only consider packages matching REGEXP')
+ @cmdln.option('-n', '--show-not-in-old', action='store_true',
+ help='show packages only in the new project')
+ @cmdln.option('-o', '--show-not-in-new', action='store_true',
+ help='show packages only in the old project')
+ @cmdln.option('-u', '--unified', action='store_true',
+ help='show full unified diffs of differences')
+ @cmdln.option('-d', '--diffstat', action='store_true',
+ help='show diffstat of differences')
+
+ def do_prdiff(self, subcmd, opts, *args):
+ """${cmd_name}: Server-side diff of two projects
+
+ Compares two projects and either summarises or outputs the
+ differences in full. In the second form, a project is compared
+ with one of its branches inside a home:$USER project (the branch
+ is treated as NEWPRJ). The home branch is optional if the current
+ working directory is a checked out copy of it.
+
+ Usage:
+ osc prdiff [OPTIONS] OLDPRJ NEWPRJ
+ osc prdiff [OPTIONS] [home:$USER:branch:$PRJ]
+
+ ${cmd_option_list}
+ """
+
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ if len(args) == 0:
+ if is_project_dir(os.curdir):
+ newprj = Project('.', getPackageList=False).name
+ oldprj = self._get_branch_parent(newprj)
+ if oldprj is None:
+ raise oscerr.WrongArgs('Current directory is not a valid home branch.')
+ else:
+ raise oscerr.WrongArgs('Current directory is not a project.')
+ elif len(args) == 1:
+ newprj = args[0]
+ oldprj = self._get_branch_parent(newprj)
+ if oldprj is None:
+ raise oscerr.WrongArgs('Single-argument form must be for a home branch.')
+ elif len(args) == 2:
+ oldprj, newprj = args
+ else:
+ raise RuntimeError('BUG in argument parsing, please report.\n'
+ 'args: ' + repr(args))
+
+ if opts.diffstat and opts.unified:
+ print('error - cannot specify both --diffstat and --unified', file=sys.stderr)
+ sys.exit(1)
+
+ apiurl = self.get_api_url()
+
+ old_packages = meta_get_packagelist(apiurl, oldprj)
+ new_packages = meta_get_packagelist(apiurl, newprj)
+
+ if opts.requests:
+ requests = get_request_list(apiurl, project=oldprj,
+ req_state=('new', 'review'))
+
+ for pkg in old_packages:
+ if self._prdiff_skip_package(opts, pkg):
+ continue
+
+ if pkg not in new_packages:
+ if opts.show_not_in_new:
+ print("old only: %s" % pkg)
+ continue
+
+ rdiff = server_diff_noex(
+ apiurl,
+ oldprj, pkg, None,
+ newprj, pkg, None,
+ unified=True, missingok=False, meta=False, expand=True
+ )
+
+ if rdiff:
+ print("differs: %s" % pkg)
+ self._prdiff_output_diff(opts, rdiff)
+
+ if opts.requests:
+ self._prdiff_output_matching_requests(opts, requests,
+ newprj, pkg)
+ else:
+ print("identical: %s" % pkg)
+
+ for pkg in new_packages:
+ if self._prdiff_skip_package(opts, pkg):
+ continue
+
+ if pkg not in old_packages:
+ if opts.show_not_in_old:
+ print("new only: %s" % pkg)
+
+
+ def do_repourls(self, subcmd, opts, *args):
+ """${cmd_name}: Shows URLs of .repo files
+
+ Shows URLs on which to access the project .repos files (yum-style
+ metadata) on download.opensuse.org.
+
+ usage:
+ osc repourls [PROJECT]
+
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 1:
+ project = args[0]
+ elif len(args) == 0:
+ project = store_read_project('.')
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ root = ET.fromstring(''.join(show_configuration(apiurl)))
+ elm = root.find('download_url')
+ if elm is None or not elm.text:
+ raise oscerr.APIError('download_url configuration element expected')
+
+ url_tmpl = elm.text + '/%s/%s/%s.repo'
+ repos = get_repositories_of_project(apiurl, project)
+ for repo in repos:
+ print(url_tmpl % (project.replace(':', ':/'), repo, project))
+
+
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='checkout the specified revision. '
+ 'NOTE: if you checkout the complete project '
+ 'this option is ignored!')
+ @cmdln.option('-e', '--expand-link', action='store_true',
+ help='if a package is a link, check out the expanded '
+ 'sources (no-op, since this became the default)')
+ @cmdln.option('-u', '--unexpand-link', action='store_true',
+ help='if a package is a link, check out the _link file ' \
+ 'instead of the expanded sources')
+ @cmdln.option('-M', '--meta', action='store_true',
+ help='checkout out meta data instead of sources' )
+ @cmdln.option('-c', '--current-dir', action='store_true',
+ help='place PACKAGE folder in the current directory' \
+ 'instead of a PROJECT/PACKAGE directory')
+ @cmdln.option('-o', '--output-dir', metavar='outdir',
+ help='place package in the specified directory' \
+ 'instead of a PROJECT/PACKAGE directory')
+ @cmdln.option('-s', '--source-service-files', action='store_true',
+ help='Run source services.' )
+ @cmdln.option('-S', '--server-side-source-service-files', action='store_true',
+ help='Use server side generated sources instead of local generation.' )
+ @cmdln.option('-l', '--limit-size', metavar='limit_size',
+ help='Skip all files with a given size')
+ @cmdln.alias('co')
+ def do_checkout(self, subcmd, opts, *args):
+ """${cmd_name}: Check out content from the repository
+
+ Check out content from the repository server, creating a local working
+ copy.
+
+ When checking out a single package, the option --revision can be used
+ to specify a revision of the package to be checked out.
+
+ When a package is a source link, then it will be checked out in
+ expanded form. If --unexpand-link option is used, the checkout will
+ instead produce the raw _link file plus patches.
+
+ usage:
+ osc co PROJECT [PACKAGE] [FILE]
+ osc co PROJECT # entire project
+ osc co PROJECT PACKAGE # a package
+ osc co PROJECT PACKAGE FILE # single file -> to current dir
+
+ while inside a project directory:
+ osc co PACKAGE # check out PACKAGE from project
+
+ with the result of rpm -q --qf '%%{DISTURL}\\n' PACKAGE
+ osc co obs://API/PROJECT/PLATFORM/REVISION-PACKAGE
+
+ ${cmd_option_list}
+ """
+
+ if opts.unexpand_link:
+ expand_link = False
+ else:
+ expand_link = True
+
+ if not args:
+ raise oscerr.WrongArgs('Incorrect number of arguments.\n\n' \
+ + self.get_cmd_help('checkout'))
+
+ # XXX: this too openSUSE-setup specific...
+ # FIXME: this should go into ~jw/patches/osc/osc.proj_pack_20101201.diff
+ # to be available to all subcommands via @cmdline.prep(proj_pack)
+ # obs://build.opensuse.org/openSUSE:11.3/standard/fc6c25e795a89503e99d59da5dc94a79-screen
+ m = re.match(r"obs://([^/]+)/(\S+)/([^/]+)/([A-Fa-f\d]+)\-(\S+)", args[0])
+ if m and len(args) == 1:
+ apiurl = "https://" + m.group(1)
+ project = project_dir = m.group(2)
+ # platform = m.group(3)
+ opts.revision = m.group(4)
+ package = m.group(5)
+ apiurl = apiurl.replace('/build.', '/api.')
+ filename = None
+ else:
+ args = slash_split(args)
+ project = package = filename = None
+ apiurl = self.get_api_url()
+ try:
+ project = project_dir = args[0]
+ package = args[1]
+ filename = args[2]
+ except:
+ pass
+
+ if len(args) == 1 and is_project_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ project_dir = os.curdir
+ package = args[0]
+
+ rev, dummy = parseRevisionOption(opts.revision)
+ if rev == None:
+ rev = "latest"
+
+ if rev and rev != "latest" and not checkRevision(project, package, rev):
+ print('Revision \'%s\' does not exist' % rev, file=sys.stderr)
+ sys.exit(1)
+
+ if filename:
+ # Note: same logic as with 'osc cat' (not 'osc ls', which never merges!)
+ if expand_link:
+ rev = show_upstream_srcmd5(apiurl, project, package, expand=True, revision=rev)
+ get_source_file(apiurl, project, package, filename, revision=rev, progress_obj=self.download_progress)
+
+ elif package:
+ if opts.current_dir:
+ project_dir = None
+ checkout_package(apiurl, project, package, rev, expand_link=expand_link, \
+ prj_dir=project_dir, service_files = opts.source_service_files, \
+ server_service_files=opts.server_side_source_service_files, \
+ progress_obj=self.download_progress, size_limit=opts.limit_size, \
+ meta=opts.meta, outdir=opts.output_dir)
+ print_request_list(apiurl, project, package)
+
+ elif project:
+ prj_dir = project
+ if sys.platform[:3] == 'win':
+ prj_dir = prj_dir.replace(':', ';')
+ if os.path.exists(prj_dir):
+ sys.exit('osc: project \'%s\' already exists' % project)
+
+ # check if the project does exist (show_project_meta will throw an exception)
+ show_project_meta(apiurl, project)
+
+ if opts.output_dir is not None:
+ init_dir=opts.output_dir
+ else:
+ init_dir=prj_dir
+ Project.init_project(apiurl, init_dir, project, conf.config['do_package_tracking'])
+ print(statfrmt('A', prj_dir))
+
+ # all packages
+ for package in meta_get_packagelist(apiurl, project):
+ if opts.output_dir is not None:
+ outputdir = os.path.join(opts.output_dir, package)
+ if not os.path.exists(opts.output_dir):
+ os.mkdir(os.path.join(opts.output_dir))
+ else:
+ outputdir=None
+
+ # don't check out local links by default
+ try:
+ m = show_files_meta(apiurl, project, package)
+ li = Linkinfo()
+ li.read(ET.fromstring(''.join(m)).find('linkinfo'))
+ if not li.haserror():
+ if li.project == project:
+ print(statfrmt('S', package + " link to package " + li.package))
+ continue
+ except:
+ pass
+
+ try:
+ checkout_package(apiurl, project, package, expand_link = expand_link, \
+ prj_dir = prj_dir, service_files = opts.source_service_files, \
+ server_service_files = opts.server_side_source_service_files, \
+ progress_obj=self.download_progress, size_limit=opts.limit_size, \
+ meta=opts.meta,outdir=outputdir)
+ except oscerr.LinkExpandError as e:
+ print('Link cannot be expanded:\n', e, file=sys.stderr)
+ print('Use "osc repairlink" for fixing merge conflicts:\n', file=sys.stderr)
+ # check out in unexpanded form at least
+ checkout_package(apiurl, project, package, expand_link = False, \
+ prj_dir = prj_dir, service_files = opts.source_service_files, \
+ server_service_files = opts.server_side_source_service_files, \
+ progress_obj=self.download_progress, size_limit=opts.limit_size, \
+ meta=opts.meta)
+ print_request_list(apiurl, project)
+
+ else:
+ raise oscerr.WrongArgs('Missing argument.\n\n' \
+ + self.get_cmd_help('checkout'))
+
+
+ @cmdln.option('-q', '--quiet', action='store_true',
+ help='print as little as possible')
+ @cmdln.option('-v', '--verbose', action='store_true',
+ help='print extra information')
+ @cmdln.option('-e', '--show-excluded', action='store_true',
+ help='also show files which are excluded by the ' \
+ '"exclude_glob" config option')
+ @cmdln.alias('st')
+ def do_status(self, subcmd, opts, *args):
+ """${cmd_name}: Show status of files in working copy
+
+ Show the status of files in a local working copy, indicating whether
+ files have been changed locally, deleted, added, ...
+
+ The first column in the output specifies the status and is one of the
+ following characters:
+ ' ' no modifications
+ 'A' Added
+ 'C' Conflicted
+ 'D' Deleted
+ 'M' Modified
+ '?' item is not under version control
+ '!' item is missing (removed by non-osc command) or incomplete
+ 'S' item is skipped (item exceeds a file size limit or is _service:* file)
+ 'F' Frozen (use "osc pull" to merge conflicts) (package-only state)
+
+ examples:
+ osc st
+ osc st <directory>
+ osc st file1 file2 ...
+
+ usage:
+ osc status [OPTS] [PATH...]
+ ${cmd_option_list}
+ """
+
+ if opts.quiet and opts.verbose:
+ raise oscerr.WrongOptions('\'--quiet\' and \'--verbose\' are mutually exclusive')
+
+ args = parseargs(args)
+ lines = []
+ excl_states = (' ',)
+ if opts.quiet:
+ excl_states += ('?',)
+ elif opts.verbose:
+ excl_states = ()
+ for arg in args:
+ if is_project_dir(arg):
+ prj = Project(arg, False)
+ # don't exclude packages with state ' ' because the packages
+ # might have modified etc. files
+ prj_excl = [st for st in excl_states if st != ' ']
+ for st, pac in sorted(prj.get_status(*prj_excl), lambda x, y: cmp(x[1], y[1])):
+ p = prj.get_pacobj(pac)
+ if p is None:
+ # state is != ' '
+ lines.append(statfrmt(st, os.path.normpath(os.path.join(prj.dir, pac))))
+ continue
+ if p.isfrozen():
+ lines.append(statfrmt('F', os.path.normpath(os.path.join(prj.dir, pac))))
+ elif st == ' ' and opts.verbose or st != ' ':
+ lines.append(statfrmt(st, os.path.normpath(os.path.join(prj.dir, pac))))
+ states = p.get_status(opts.show_excluded, *excl_states)
+ for st, filename in sorted(states, lambda x, y: cmp(x[1], y[1])):
+ lines.append(statfrmt(st, os.path.normpath(os.path.join(p.dir, filename))))
+ else:
+ p = findpacs([arg])[0]
+ for st, filename in sorted(p.get_status(opts.show_excluded, *excl_states), lambda x, y: cmp(x[1], y[1])):
+ lines.append(statfrmt(st, os.path.normpath(os.path.join(p.dir, filename))))
+ if lines:
+ print('\n'.join(lines))
+
+
+ def do_add(self, subcmd, opts, *args):
+ """${cmd_name}: Mark files to be added upon the next commit
+
+ In case a URL is given the file will get downloaded and registered to be downloaded
+ by the server as well via the download_url source service.
+
+ This is recommended for release tar balls to track their source and to help
+ others to review your changes esp. on version upgrades.
+
+ usage:
+ osc add URL [URL...]
+ osc add FILE [FILE...]
+ ${cmd_option_list}
+ """
+ if not args:
+ raise oscerr.WrongArgs('Missing argument.\n\n' \
+ + self.get_cmd_help('add'))
+
+ # Do some magic here, when adding a url. We want that the server to download the tar ball and to verify it
+ for arg in parseargs(args):
+ if arg.startswith('http://') or arg.startswith('https://') or arg.startswith('ftp://') or arg.startswith('git://'):
+ if arg.endswith('.git'):
+ addGitSource(arg)
+ else:
+ addDownloadUrlService(arg)
+ else:
+ addFiles([arg])
+
+
+ def do_mkpac(self, subcmd, opts, *args):
+ """${cmd_name}: Create a new package under version control
+
+ usage:
+ osc mkpac new_package
+ ${cmd_option_list}
+ """
+ if not conf.config['do_package_tracking']:
+ print("to use this feature you have to enable \'do_package_tracking\' " \
+ "in the [general] section in the configuration file", file=sys.stderr)
+ sys.exit(1)
+
+ if len(args) != 1:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ createPackageDir(args[0])
+
+ @cmdln.option('-r', '--recursive', action='store_true',
+ help='If CWD is a project dir then scan all package dirs as well')
+ @cmdln.alias('ar')
+ def do_addremove(self, subcmd, opts, *args):
+ """${cmd_name}: Adds new files, removes disappeared files
+
+ Adds all files new in the local copy, and removes all disappeared files.
+
+ ARG, if specified, is a package working copy.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ args = parseargs(args)
+ arg_list = args[:]
+ for arg in arg_list:
+ if is_project_dir(arg) and conf.config['do_package_tracking']:
+ prj = Project(arg, False)
+ for pac in prj.pacs_unvers:
+ pac_dir = getTransActPath(os.path.join(prj.dir, pac))
+ if os.path.isdir(pac_dir):
+ addFiles([pac_dir], prj)
+ for pac in prj.pacs_broken:
+ if prj.get_state(pac) != 'D':
+ prj.set_state(pac, 'D')
+ print(statfrmt('D', getTransActPath(os.path.join(prj.dir, pac))))
+ if opts.recursive:
+ for pac in prj.pacs_have:
+ state = prj.get_state(pac)
+ if state != None and state != 'D':
+ pac_dir = getTransActPath(os.path.join(prj.dir, pac))
+ args.append(pac_dir)
+ args.remove(arg)
+ prj.write_packages()
+ elif is_project_dir(arg):
+ print('osc: addremove is not supported in a project dir unless ' \
+ '\'do_package_tracking\' is enabled in the configuration file', file=sys.stderr)
+ sys.exit(1)
+
+ pacs = findpacs(args)
+ for p in pacs:
+ todo = list(set(p.filenamelist + p.filenamelist_unvers + p.to_be_added))
+ for filename in todo:
+ abs_filename = os.path.join(p.absdir, filename)
+ if os.path.isdir(abs_filename):
+ continue
+ # ignore foo.rXX, foo.mine for files which are in 'C' state
+ if os.path.splitext(filename)[0] in p.in_conflict:
+ continue
+ state = p.status(filename)
+ if state == '?':
+ # TODO: should ignore typical backup files suffix ~ or .orig
+ p.addfile(filename)
+ elif state == 'D' and os.path.isfile(abs_filename):
+ # if the "deleted" file exists in the wc, track it again
+ p.addfile(filename)
+ elif state == '!':
+ p.delete_file(filename)
+ print(statfrmt('D', getTransActPath(os.path.join(p.dir, filename))))
+
+ @cmdln.alias('ci')
+ @cmdln.alias('checkin')
+ @cmdln.option('-m', '--message', metavar='TEXT',
+ help='specify log message TEXT')
+ @cmdln.option('-n', '--no-message', default=False, action='store_true',
+ help='do not specify a log message')
+ @cmdln.option('-F', '--file', metavar='FILE',
+ help='read log message from FILE, \'-\' denotes standard input.')
+ @cmdln.option('-f', '--force', default=False, action="store_true",
+ help='force commit, even if there were no changes')
+ @cmdln.option('--skip-validation', default=False, action="store_true",
+ help='deprecated, don\'t use it')
+ @cmdln.option('-v', '--verbose', default=False, action="store_true",
+ help='Run the source services with verbose information')
+ @cmdln.option('--skip-local-service-run', '--noservice', default=False, action="store_true",
+ help='Skip service run of configured source services for local run')
+ def do_commit(self, subcmd, opts, *args):
+ """${cmd_name}: Upload content to the repository server
+
+ Upload content which is changed in your working copy, to the repository
+ server.
+
+ examples:
+ osc ci # current dir
+ osc ci <dir>
+ osc ci file1 file2 ...
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ args = parseargs(args)
+
+ if opts.skip_validation:
+ print("WARNING: deprecated option --skip-validation ignored.", file=sys.stderr)
+
+ msg = ''
+ if opts.message:
+ msg = opts.message
+ elif opts.file:
+ if opts.file == '-':
+ msg = sys.stdin.read()
+ else:
+ try:
+ msg = open(opts.file).read()
+ except:
+ sys.exit('could not open file \'%s\'.' % opts.file)
+ skip_local_service_run = False
+ if not conf.config['local_service_run'] or opts.skip_local_service_run:
+ skip_local_service_run = True
+ arg_list = args[:]
+ for arg in arg_list:
+ if conf.config['do_package_tracking'] and is_project_dir(arg):
+ try:
+ prj = Project(arg)
+ if not msg and not opts.no_message:
+ msg = edit_message()
+
+ # check any of the packages is a link, if so, as for branching
+ pacs = (Package(os.path.join(prj.dir, pac))
+ for pac in prj.pacs_have if prj.get_state(pac) == ' ')
+ can_branch = False
+ if any(pac.is_link_to_different_project() for pac in pacs):
+ repl = raw_input('Some of the packages are links to a different project!\n' \
+ 'Create a local branch before commit? (y|N) ')
+ if repl in('y', 'Y'):
+ can_branch = True
+
+ prj.commit(msg=msg, skip_local_service_run=skip_local_service_run, verbose=opts.verbose, can_branch=can_branch)
+ except oscerr.ExtRuntimeError as e:
+ print("ERROR: service run failed", e, file=sys.stderr)
+ return 1
+ args.remove(arg)
+
+ pacs = findpacs(args)
+
+ if conf.config['do_package_tracking'] and len(pacs) > 0:
+ prj_paths = {}
+ single_paths = []
+ files = {}
+ # XXX: this is really ugly
+ pac_objs = {}
+ # it is possible to commit packages from different projects at the same
+ # time: iterate over all pacs and put each pac to the right project in the dict
+ for pac in pacs:
+ path = os.path.normpath(os.path.join(pac.dir, os.pardir))
+ if is_project_dir(path):
+ pac_path = os.path.basename(os.path.normpath(pac.absdir))
+ prj_paths.setdefault(path, []).append(pac_path)
+ pac_objs.setdefault(path, []).append(pac)
+ files[pac_path] = pac.todo
+ else:
+ single_paths.append(pac.dir)
+ if not pac.todo:
+ pac.todo = pac.filenamelist + pac.filenamelist_unvers
+ pac.todo.sort()
+ for prj_path, packages in prj_paths.items():
+ prj = Project(prj_path)
+ if not msg and not opts.no_message:
+ msg = get_commit_msg(prj.absdir, pac_objs[prj_path])
+
+ # check any of the packages is a link, if so, as for branching
+ can_branch = False
+ if any(pac.is_link_to_different_project() for pac in pacs):
+ repl = raw_input('Some of the packages are links to a different project!\n' \
+ 'Create a local branch before commit? (y|N) ')
+ if repl in('y', 'Y'):
+ can_branch = True
+
+ prj.commit(packages, msg=msg, files=files, skip_local_service_run=skip_local_service_run, verbose=opts.verbose, can_branch=can_branch, force=opts.force)
+ store_unlink_file(prj.absdir, '_commit_msg')
+ for pac in single_paths:
+ p = Package(pac)
+ if not msg and not opts.no_message:
+ msg = get_commit_msg(p.absdir, [p])
+ p.commit(msg, skip_local_service_run=skip_local_service_run, verbose=opts.verbose, force=opts.force)
+ store_unlink_file(p.absdir, '_commit_msg')
+ else:
+ for p in pacs:
+ if not p.todo:
+ p.todo = p.filenamelist + p.filenamelist_unvers
+ p.todo.sort()
+ if not msg and not opts.no_message:
+ msg = get_commit_msg(p.absdir, [p])
+ p.commit(msg, skip_local_service_run=skip_local_service_run, verbose=opts.verbose, force=opts.force)
+ store_unlink_file(p.absdir, '_commit_msg')
+
+ @cmdln.option('-r', '--revision', metavar='REV',
+ help='update to specified revision (this option will be ignored '
+ 'if you are going to update the complete project or more than '
+ 'one package)')
+ @cmdln.option('-u', '--unexpand-link', action='store_true',
+ help='if a package is an expanded link, update to the raw _link file')
+ @cmdln.option('-e', '--expand-link', action='store_true',
+ help='if a package is a link, update to the expanded sources')
+ @cmdln.option('-s', '--source-service-files', action='store_true',
+ help='Run local source services after update.' )
+ @cmdln.option('-S', '--server-side-source-service-files', action='store_true',
+ help='Use server side generated sources instead of local generation.' )
+ @cmdln.option('-l', '--limit-size', metavar='limit_size',
+ help='Skip all files with a given size')
+ @cmdln.alias('up')
+ def do_update(self, subcmd, opts, *args):
+ """${cmd_name}: Update a working copy
+
+ examples:
+
+ 1. osc up
+ If the current working directory is a package, update it.
+ If the directory is a project directory, update all contained
+ packages, AND check out newly added packages.
+
+ To update only checked out packages, without checking out new
+ ones, you might want to use "osc up *" from within the project
+ dir.
+
+ 2. osc up PAC
+ Update the packages specified by the path argument(s)
+
+ When --expand-link is used with source link packages, the expanded
+ sources will be checked out. Without this option, the _link file and
+ patches will be checked out. The option --unexpand-link can be used to
+ switch back to the "raw" source with a _link file plus patch(es).
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ if (opts.expand_link and opts.unexpand_link) \
+ or (opts.expand_link and opts.revision) \
+ or (opts.unexpand_link and opts.revision):
+ raise oscerr.WrongOptions('Sorry, the options --expand-link, --unexpand-link and '
+ '--revision are mutually exclusive.')
+
+ args = parseargs(args)
+ arg_list = args[:]
+
+ for arg in arg_list:
+ if is_project_dir(arg):
+ prj = Project(arg, progress_obj=self.download_progress)
+
+ if conf.config['do_package_tracking']:
+ prj.update(expand_link=opts.expand_link,
+ unexpand_link=opts.unexpand_link)
+ args.remove(arg)
+ else:
+ # if not tracking package, and 'update' is run inside a project dir,
+ # it should do the following:
+ # (a) update all packages
+ args += prj.pacs_have
+ # (b) fetch new packages
+ prj.checkout_missing_pacs(expand_link = not opts.unexpand_link)
+ args.remove(arg)
+ print_request_list(prj.apiurl, prj.name)
+
+ args.sort()
+ pacs = findpacs(args, progress_obj=self.download_progress)
+
+ if opts.revision and len(args) == 1:
+ rev, dummy = parseRevisionOption(opts.revision)
+ if not checkRevision(pacs[0].prjname, pacs[0].name, rev, pacs[0].apiurl):
+ print('Revision \'%s\' does not exist' % rev, file=sys.stderr)
+ sys.exit(1)
+ else:
+ rev = None
+
+ for p in pacs:
+ if len(pacs) > 1:
+ print('Updating %s' % p.name)
+
+ # this shouldn't be needed anymore with the new update mechanism
+ # an expand/unexpand update is treated like a normal update (there's nothing special)
+ # FIXME: ugly workaround for #399247
+# if opts.expand_link or opts.unexpand_link:
+# if [ i for i in p.filenamelist+p.filenamelist_unvers if p.status(i) != ' ' and p.status(i) != '?']:
+# print >>sys.stderr, 'osc: cannot expand/unexpand because your working ' \
+# 'copy has local modifications.\nPlease revert/commit them ' \
+# 'and try again.'
+# sys.exit(1)
+
+ if not rev:
+ if opts.expand_link:
+ rev = p.latest_rev(expand=True)
+ if p.islink() and not p.isexpanded():
+ print('Expanding to rev', rev)
+ elif opts.unexpand_link and p.islink() and p.isexpanded():
+ rev = show_upstream_rev(p.apiurl, p.prjname, p.name, meta=p.meta)
+ print('Unexpanding to rev', rev)
+ elif (p.islink() and p.isexpanded()) or opts.server_side_source_service_files:
+ rev = p.latest_rev(include_service_files=opts.server_side_source_service_files)
+
+ p.update(rev, opts.server_side_source_service_files, opts.limit_size)
+ if opts.source_service_files:
+ print('Running local source services')
+ p.run_source_services()
+ if opts.unexpand_link:
+ p.unmark_frozen()
+ rev = None
+ print_request_list(p.apiurl, p.prjname, p.name)
+
+
+ @cmdln.option('-f', '--force', action='store_true',
+ help='forces removal of entire package and its files')
+ @cmdln.alias('rm')
+ @cmdln.alias('del')
+ @cmdln.alias('remove')
+ def do_delete(self, subcmd, opts, *args):
+ """${cmd_name}: Mark files or package directories to be deleted upon the next 'checkin'
+
+ usage:
+ cd .../PROJECT/PACKAGE
+ osc delete FILE [...]
+ cd .../PROJECT
+ osc delete PACKAGE [...]
+
+ This command works on check out copies. Use "rdelete" for working on server
+ side only. This is needed for removing the entire project.
+
+ As a safety measure, projects must be empty (i.e., you need to delete all
+ packages first).
+
+ If you are sure that you want to remove a package and all
+ its files use \'--force\' switch. Sometimes this also works without --force.
+
+ ${cmd_option_list}
+ """
+
+ if not args:
+ raise oscerr.WrongArgs('Missing argument.\n\n' \
+ + self.get_cmd_help('delete'))
+
+ args = parseargs(args)
+ # check if args contains a package which was removed by
+ # a non-osc command and mark it with the 'D'-state
+ arg_list = args[:]
+ for i in arg_list:
+ if not os.path.exists(i):
+ prj_dir, pac_dir = getPrjPacPaths(i)
+ if is_project_dir(prj_dir):
+ prj = Project(prj_dir, False)
+ if i in prj.pacs_broken:
+ if prj.get_state(i) != 'A':
+ prj.set_state(pac_dir, 'D')
+ else:
+ prj.del_package_node(i)
+ print(statfrmt('D', getTransActPath(i)))
+ args.remove(i)
+ prj.write_packages()
+ pacs = findpacs(args)
+
+ for p in pacs:
+ if not p.todo:
+ prj_dir, pac_dir = getPrjPacPaths(p.absdir)
+ if is_project_dir(prj_dir):
+ if conf.config['do_package_tracking']:
+ prj = Project(prj_dir, False)
+ prj.delPackage(p, opts.force)
+ else:
+ print("WARNING: package tracking is disabled, operation skipped !", file=sys.stderr)
+ else:
+ pathn = getTransActPath(p.dir)
+ for filename in p.todo:
+ p.clear_from_conflictlist(filename)
+ ret, state = p.delete_file(filename, opts.force)
+ if ret:
+ print(statfrmt('D', os.path.join(pathn, filename)))
+ continue
+ if state == '?':
+ sys.exit('\'%s\' is not under version control' % filename)
+ elif state in ['A', 'M'] and not opts.force:
+ sys.exit('\'%s\' has local modifications (use --force to remove this file)' % filename)
+ elif state == 'S':
+ sys.exit('\'%s\' is marked as skipped and no local file with this name exists' % filename)
+
+
+ def do_resolved(self, subcmd, opts, *args):
+ """${cmd_name}: Remove 'conflicted' state on working copy files
+
+ If an upstream change can't be merged automatically, a file is put into
+ in 'conflicted' ('C') state. Within the file, conflicts are marked with
+ special <<<<<<< as well as ======== and >>>>>>> lines.
+
+ After manually resolving all conflicting parts, use this command to
+ remove the 'conflicted' state.
+
+ Note: this subcommand does not semantically resolve conflicts or
+ remove conflict markers; it merely removes the conflict-related
+ artifact files and allows PATH to be committed again.
+
+ usage:
+ osc resolved FILE [FILE...]
+ ${cmd_option_list}
+ """
+
+ if not args:
+ raise oscerr.WrongArgs('Missing argument.\n\n' \
+ + self.get_cmd_help('resolved'))
+
+ args = parseargs(args)
+ pacs = findpacs(args)
+
+ for p in pacs:
+ for filename in p.todo:
+ print('Resolved conflicted state of "%s"' % filename)
+ p.clear_from_conflictlist(filename)
+
+
+ @cmdln.alias('dists')
+# FIXME: using just ^DISCONTINUED as match is not a general approach and only valid for one instance
+# we need to discuss an api call for that, if we need this
+# @cmdln.option('-d', '--discontinued', action='store_true',
+# help='show discontinued distributions')
+ def do_distributions(self, subcmd, opts, *args):
+ """${cmd_name}: Shows all available distributions
+
+ This command shows the available distributions. For active distributions
+ it shows the name, project and name of the repository and a suggested default repository name.
+
+ usage:
+ osc distributions
+
+ ${cmd_option_list}
+ """
+ apiurl = self.get_api_url()
+
+ print('\n'.join(get_distibutions(apiurl)))#FIXME:, opts.discontinued))
+
+ @cmdln.hide(1)
+ def do_results_meta(self, subcmd, opts, *args):
+ print("Command results_meta is obsolete. Please use: osc results --xml")
+ sys.exit(1)
+
+ @cmdln.hide(1)
+ @cmdln.option('-l', '--last-build', action='store_true',
+ help='show last build results (succeeded/failed/unknown)')
+ @cmdln.option('-r', '--repo', action='append', default = [],
+ help='Show results only for specified repo(s)')
+ @cmdln.option('-a', '--arch', action='append', default = [],
+ help='Show results only for specified architecture(s)')
+ @cmdln.option('', '--xml', action='store_true',
+ help='generate output in XML (former results_meta)')
+ def do_rresults(self, subcmd, opts, *args):
+ print("Command rresults is obsolete. Running 'osc results' instead")
+ self.do_results('results', opts, *args)
+ sys.exit(1)
+
+
+ @cmdln.option('-f', '--force', action='store_true', default=False,
+ help="Don't ask and delete files")
+ def do_rremove(self, subcmd, opts, project, package, *files):
+ """${cmd_name}: Remove source files from selected package
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ apiurl = self.get_api_url()
+
+ if len(files) == 0:
+ if not '/' in project:
+ raise oscerr.WrongArgs("Missing operand, type osc help rremove for help")
+ else:
+ files = (package, )
+ project, package = project.split('/')
+
+ for filename in files:
+ if not opts.force:
+ resp = raw_input("rm: remove source file `%s' from `%s/%s'? (yY|nN) " % (filename, project, package))
+ if resp not in ('y', 'Y'):
+ continue
+ try:
+ delete_files(apiurl, project, package, (filename, ))
+ except HTTPError as e:
+ if opts.force:
+ print(e, file=sys.stderr)
+ body = e.read()
+ if e.code in [ 400, 403, 404, 500 ]:
+ if '<summary>' in body:
+ msg = body.split('<summary>')[1]
+ msg = msg.split('</summary>')[0]
+ print(msg, file=sys.stderr)
+ else:
+ raise e
+
+ @cmdln.alias('r')
+ @cmdln.option('-l', '--last-build', action='store_true',
+ help='show last build results (succeeded/failed/unknown)')
+ @cmdln.option('-r', '--repo', action='append', default = [],
+ help='Show results only for specified repo(s)')
+ @cmdln.option('-a', '--arch', action='append', default = [],
+ help='Show results only for specified architecture(s)')
+ @cmdln.option('-v', '--verbose', action='store_true', default=False,
+ help='more verbose output')
+ @cmdln.option('-w', '--watch', action='store_true', default=False,
+ help='watch the results until all finished building')
+ @cmdln.option('', '--xml', action='store_true', default=False,
+ help='generate output in XML (former results_meta)')
+ @cmdln.option('', '--csv', action='store_true', default=False,
+ help='generate output in CSV format')
+ @cmdln.option('', '--format', default='%(repository)s|%(arch)s|%(state)s|%(dirty)s|%(code)s|%(details)s',
+ help='format string for csv output')
+ def do_results(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build results of a package or project
+
+ Usage:
+ osc results # (inside working copy of PRJ or PKG)
+ osc results PROJECT [PACKAGE]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ apiurl = self.get_api_url()
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments (required none, one, or two)')
+ project = package = None
+ wd = os.curdir
+ if is_project_dir(wd):
+ project = store_read_project(wd)
+ elif is_package_dir(wd):
+ project = store_read_project(wd)
+ package = store_read_package(wd)
+ if len(args) > 0:
+ project = args[0]
+ if len(args) > 1:
+ package = args[1]
+
+ if project == None:
+ raise oscerr.WrongOptions("No project given")
+
+ if package == None:
+ if opts.arch == []:
+ opts.arch = None
+ if opts.repo == []:
+ opts.repo = None
+ opts.hide_legend = None
+ opts.name_filter = None
+ opts.status_filter = None
+ opts.vertical = None
+ opts.show_non_building = None
+ opts.show_excluded = None
+ self.do_prjresults('prjresults', opts, *args)
+ return
+
+ if opts.xml and opts.csv:
+ raise oscerr.WrongOptions("--xml and --csv are mutual exclusive")
+
+ kwargs = {'apiurl': apiurl, 'project': project, 'package': package,
+ 'lastbuild': opts.last_build, 'repository': opts.repo,
+ 'arch': opts.arch, 'wait': opts.watch}
+ if opts.xml or opts.csv:
+ for xml in get_package_results(**kwargs):
+ if opts.xml:
+ print(xml, end='')
+ else:
+ # csv formatting
+ results = result_xml_to_dicts(xml)
+ print('\n'.join(format_results(results, opts.format)))
+ else:
+ kwargs['verbose'] = opts.verbose
+ kwargs['wait'] = opts.watch
+ kwargs['printJoin'] = '\n'
+ get_results(**kwargs)
+
+
+ # WARNING: this function is also called by do_results. You need to set a default there
+ # as well when adding a new option!
+ @cmdln.option('-q', '--hide-legend', action='store_true',
+ help='hide the legend')
+ @cmdln.option('-c', '--csv', action='store_true',
+ help='csv output')
+ @cmdln.option('', '--xml', action='store_true', default=False,
+ help='generate output in XML')
+ @cmdln.option('-s', '--status-filter', metavar='STATUS',
+ help='show only packages with buildstatus STATUS (see legend)')
+ @cmdln.option('-n', '--name-filter', metavar='EXPR',
+ help='show only packages whose names match EXPR')
+ @cmdln.option('-a', '--arch', metavar='ARCH',
+ help='show results only for specified architecture(s)')
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='show results only for specified repo(s)')
+ @cmdln.option('-V', '--vertical', action='store_true',
+ help='list packages vertically instead horizontally')
+ @cmdln.option('--show-excluded', action='store_true',
+ help='show packages that are excluded in all repos, also hide repos that have only excluded packages')
+ @cmdln.alias('pr')
+ def do_prjresults(self, subcmd, opts, *args):
+ """${cmd_name}: Shows project-wide build results
+
+ Usage:
+ osc prjresults (inside working copy)
+ osc prjresults PROJECT
+
+ ${cmd_option_list}
+ """
+ apiurl = self.get_api_url()
+
+ if args:
+ if len(args) == 1:
+ project = args[0]
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+ else:
+ wd = os.curdir
+ project = store_read_project(wd)
+
+ if opts.xml:
+ print(''.join(show_prj_results_meta(apiurl, project)))
+ return
+
+ print('\n'.join(get_prj_results(apiurl, project, hide_legend=opts.hide_legend, \
+ csv=opts.csv, status_filter=opts.status_filter, \
+ name_filter=opts.name_filter, repo=opts.repo, \
+ arch=opts.arch, vertical=opts.vertical, \
+ show_excluded=opts.show_excluded)))
+
+ @cmdln.option('-q', '--hide-legend', action='store_true',
+ help='hide the legend')
+ @cmdln.option('-c', '--csv', action='store_true',
+ help='csv output')
+ @cmdln.option('-s', '--status-filter', metavar='STATUS',
+ help='show only packages with buildstatus STATUS (see legend)')
+ @cmdln.option('-n', '--name-filter', metavar='EXPR',
+ help='show only packages whose names match EXPR')
+
+ @cmdln.hide(1)
+ def do_rprjresults(self, subcmd, opts, *args):
+ print("Command rprjresults is obsolete. Please use 'osc prjresults'")
+ sys.exit(1)
+
+ @cmdln.alias('bl')
+ @cmdln.alias('blt')
+ @cmdln.alias('buildlogtail')
+ @cmdln.option('-l', '--last', action='store_true',
+ help='Show the last finished log file')
+ @cmdln.option('-o', '--offset', metavar='OFFSET',
+ help='get log start or end from the offset')
+ @cmdln.option('-s', '--strip-time', action='store_true',
+ help='strip leading build time from the log')
+ def do_buildlog(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build log of a package
+
+ Shows the log file of the build of a package. Can be used to follow the
+ log while it is being written.
+ Needs to be called from within a package directory.
+
+ When called as buildlogtail (or blt) it just shows the end of the logfile.
+ This is useful to see just a build failure reasons.
+
+ The arguments REPOSITORY and ARCH are the first two columns in the 'osc
+ results' output. If the buildlog url is used buildlog command has the
+ same behavior as remotebuildlog.
+
+ ${cmd_usage} [REPOSITORY ARCH | BUILDLOGURL]
+ ${cmd_option_list}
+ """
+ import osc.build
+
+ project = package = repository = arch = None
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 1 and args[0].startswith('http'):
+ apiurl, project, package, repository, arch = parse_buildlogurl(args[0])
+ else:
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ if len(args) == 1:
+ repository, arch = self._find_last_repo_arch(args[0], fatal=False)
+ if repository is None:
+ # no local build with this repo was done
+ print('failed to guess arch, using hostarch')
+ repository = args[0]
+ arch = osc.build.hostarch
+ elif len(args) < 2:
+ self.print_repos()
+ elif len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+ else:
+ repository = args[0]
+ arch = args[1]
+
+ offset = 0
+ if subcmd == "blt" or subcmd == "buildlogtail":
+ query = { 'view': 'entry' }
+ if opts.last:
+ query['last'] = 1
+ u = makeurl(self.get_api_url(), ['build', project, repository, arch, package, '_log'], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ offset = int(root.find('entry').get('size'))
+ if opts.offset:
+ offset = offset - int(opts.offset)
+ else:
+ offset = offset - ( 8 * 1024 )
+ if offset < 0:
+ offset = 0
+ elif opts.offset:
+ offset = int(opts.offset)
+ strip_time = opts.strip_time or conf.config['buildlog_strip_time']
+ print_buildlog(apiurl, project, package, repository, arch, offset, strip_time, opts.last)
+
+
+ def print_repos(self, repos_only=False, exc_class=oscerr.WrongArgs, exc_msg='Missing arguments'):
+ wd = os.curdir
+ doprint = False
+ if is_package_dir(wd):
+ msg = "package"
+ doprint = True
+ elif is_project_dir(wd):
+ msg = "project"
+ doprint = True
+
+ if doprint:
+ print('Valid arguments for this %s are:' % msg)
+ print()
+ if repos_only:
+ self.do_repositories("repos_only", None)
+ else:
+ self.do_repositories(None, None)
+ raise exc_class(exc_msg)
+
+ @cmdln.alias('rbl')
+ @cmdln.alias('rbuildlog')
+ @cmdln.alias('rblt')
+ @cmdln.alias('rbuildlogtail')
+ @cmdln.alias('remotebuildlogtail')
+ @cmdln.option('-l', '--last', action='store_true',
+ help='Show the last finished log file')
+ @cmdln.option('-o', '--offset', metavar='OFFSET',
+ help='get log starting or ending from the offset')
+ @cmdln.option('-s', '--strip-time', action='store_true',
+ help='strip leading build time from the log')
+ def do_remotebuildlog(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build log of a package
+
+ Shows the log file of the build of a package. Can be used to follow the
+ log while it is being written.
+
+ remotebuildlogtail shows just the tail of the log file.
+
+ usage:
+ osc remotebuildlog project package repository arch
+ or
+ osc remotebuildlog project/package/repository/arch
+ or
+ osc remotebuildlog buildlogurl
+ ${cmd_option_list}
+ """
+ if len(args) == 1 and args[0].startswith('http'):
+ apiurl, project, package, repository, arch = parse_buildlogurl(args[0])
+ else:
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+ if len(args) < 4:
+ raise oscerr.WrongArgs('Too few arguments.')
+ elif len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+ else:
+ project, package, repository, arch = args
+
+ offset = 0
+ if subcmd == "rblt" or subcmd == "rbuildlogtail" or subcmd == "remotebuildlogtail":
+ query = { 'view': 'entry' }
+ if opts.last:
+ query['last'] = 1
+ u = makeurl(self.get_api_url(), ['build', project, repository, arch, package, '_log'], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ offset = int(root.find('entry').get('size'))
+ if opts.offset:
+ offset = offset - int(opts.offset)
+ else:
+ offset = offset - ( 8 * 1024 )
+ if offset < 0:
+ offset = 0
+ elif opts.offset:
+ offset = int(opts.offset)
+ strip_time = opts.strip_time or conf.config['buildlog_strip_time']
+ print_buildlog(apiurl, project, package, repository, arch, offset, strip_time, opts.last)
+
+ def _find_last_repo_arch(self, repo=None, fatal=True):
+ import glob
+ files = glob.glob(os.path.join(os.getcwd(), store, "_buildinfo-*"))
+ if repo is not None:
+ files = [f for f in files
+ if os.path.basename(f).replace('_buildinfo-', '').startswith(repo + '-')]
+ if not files:
+ if not fatal:
+ return None, None
+ self.print_repos()
+ cfg = files[0]
+ # find newest file
+ for f in files[1:]:
+ if os.stat(f).st_atime > os.stat(cfg).st_atime:
+ cfg = f
+ root = ET.parse(cfg).getroot()
+ repo = root.get("repository")
+ arch = root.find("arch").text
+ return repo, arch
+
+ @cmdln.alias('lbl')
+ @cmdln.option('-o', '--offset', metavar='OFFSET',
+ help='get log starting from offset')
+ @cmdln.option('-s', '--strip-time', action='store_true',
+ help='strip leading build time from the log')
+ def do_localbuildlog(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build log of a local buildchroot
+
+ usage:
+ osc lbl [REPOSITORY [ARCH]]
+ osc lbl # show log of newest last local build
+
+ ${cmd_option_list}
+ """
+ if conf.config['build-type']:
+ # FIXME: raise Exception instead
+ print('Not implemented for VMs', file=sys.stderr)
+ sys.exit(1)
+
+ if len(args) == 0 or len(args) == 1:
+ project = store_read_project('.')
+ package = store_read_package('.')
+ repo = None
+ if args:
+ repo = args[0]
+ repo, arch = self._find_last_repo_arch(repo)
+ elif len(args) == 2:
+ project = store_read_project('.')
+ package = store_read_package('.')
+ repo = args[0]
+ arch = args[1]
+ else:
+ if is_package_dir(os.curdir):
+ self.print_repos()
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ # TODO: refactor/unify buildroot calculation and move it to core.py
+ buildroot = os.environ.get('OSC_BUILD_ROOT', conf.config['build-root'])
+ apihost = urlsplit(self.get_api_url())[1]
+ buildroot = buildroot % {'project': project, 'package': package,
+ 'repo': repo, 'arch': arch, 'apihost': apihost}
+ offset = 0
+ if opts.offset:
+ offset = int(opts.offset)
+ logfile = os.path.join(buildroot, '.build.log')
+ if not os.path.isfile(logfile):
+ raise oscerr.OscIOError(None, 'logfile \'%s\' does not exist' % logfile)
+ f = open(logfile, 'r')
+ f.seek(offset)
+ data = f.read(BUFSIZE)
+ while len(data):
+ if opts.strip_time or conf.config['buildlog_strip_time']:
+ data = buildlog_strip_time(data)
+ sys.stdout.write(data)
+ data = f.read(BUFSIZE)
+ f.close()
+
+ @cmdln.alias('tr')
+ def do_triggerreason(self, subcmd, opts, *args):
+ """${cmd_name}: Show reason why a package got triggered to build
+
+ The server decides when a package needs to get rebuild, this command
+ shows the detailed reason for a package. A brief reason is also stored
+ in the jobhistory, which can be accessed via "osc jobhistory".
+
+ Trigger reasons might be:
+ - new build (never build yet or rebuild manually forced)
+ - source change (eg. on updating sources)
+ - meta change (packages which are used for building have changed)
+ - rebuild count sync (In case that it is configured to sync release numbers)
+
+ usage in package or project directory:
+ osc reason REPOSITORY ARCH
+ osc reason PROJECT PACKAGE REPOSITORY ARCH
+
+ ${cmd_option_list}
+ """
+ wd = os.curdir
+ args = slash_split(args)
+ project = package = repository = arch = None
+
+ if len(args) < 2:
+ self.print_repos()
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 2: # 2
+ if is_package_dir('.'):
+ package = store_read_package(wd)
+ else:
+ raise oscerr.WrongArgs('package is not specified.')
+ project = store_read_project(wd)
+ repository = args[0]
+ arch = args[1]
+ elif len(args) == 4:
+ project = args[0]
+ package = args[1]
+ repository = args[2]
+ arch = args[3]
+ else:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ print(apiurl, project, package, repository, arch)
+ xml = show_package_trigger_reason(apiurl, project, package, repository, arch)
+ root = ET.fromstring(xml)
+ reason = root.find('explain').text
+ print(reason)
+ if reason == "meta change":
+ print("changed keys:")
+ for package in root.findall('packagechange'):
+ print(" ", package.get('change'), package.get('key'))
+
+
+ # FIXME: the new osc syntax should allow to specify multiple packages
+ # FIXME: the command should optionally use buildinfo data to show all dependencies
+ @cmdln.alias('whatdependson')
+ def do_dependson(self, subcmd, opts, *args):
+ """${cmd_name}: Show the build dependencies
+
+ The command dependson and whatdependson can be used to find out what
+ will be triggered when a certain package changes.
+ This is no guarantee, since the new build might have changed dependencies.
+
+ dependson shows the build dependencies inside of a project, valid for a
+ given repository and architecture.
+ NOTE: to see all binary packages, which can trigger a build you need to
+ refer the buildinfo, since this command shows only the dependencies
+ inside of a project.
+
+ The arguments REPOSITORY and ARCH can be taken from the first two columns
+ of the 'osc repos' output.
+
+ usage in package or project directory:
+ osc dependson REPOSITORY ARCH
+ osc whatdependson REPOSITORY ARCH
+
+ usage:
+ osc dependson PROJECT [PACKAGE] REPOSITORY ARCH
+ osc whatdependson PROJECT [PACKAGE] REPOSITORY ARCH
+
+ ${cmd_option_list}
+ """
+ wd = os.curdir
+ args = slash_split(args)
+ project = packages = repository = arch = reverse = None
+
+ if len(args) < 2 and (is_package_dir('.') or is_project_dir('.')):
+ self.print_repos()
+
+ if len(args) > 4:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ apiurl = self.get_api_url()
+
+ if len(args) < 3: # 2
+ if is_package_dir('.'):
+ packages = [store_read_package(wd)]
+ elif not is_project_dir('.'):
+ raise oscerr.WrongArgs('Project and package is not specified.')
+ project = store_read_project(wd)
+ repository = args[0]
+ arch = args[1]
+
+ if len(args) == 3:
+ project = args[0]
+ repository = args[1]
+ arch = args[2]
+
+ if len(args) == 4:
+ project = args[0]
+ packages = [args[1]]
+ repository = args[2]
+ arch = args[3]
+
+ if subcmd == 'whatdependson':
+ reverse = 1
+
+ xml = get_dependson(apiurl, project, repository, arch, packages, reverse)
+
+ root = ET.fromstring(xml)
+ for package in root.findall('package'):
+ print(package.get('name'), ":")
+ for dep in package.findall('pkgdep'):
+ print(" ", dep.text)
+
+
+ @cmdln.option('-d', '--debug', action='store_true',
+ help='verbose output of build dependencies')
+ @cmdln.option('-x', '--extra-pkgs', metavar='PAC', action='append',
+ help='Add this package when computing the buildinfo')
+ @cmdln.option('-p', '--prefer-pkgs', metavar='DIR', action='append',
+ help='Prefer packages from this directory when installing the build-root')
+ def do_buildinfo(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build info
+
+ Shows the build "info" which is used in building a package.
+ This command is mostly used internally by the 'build' subcommand.
+ It needs to be called from within a package directory.
+
+ The BUILD_DESCR argument is optional. BUILD_DESCR is a local RPM specfile
+ or Debian "dsc" file. If specified, it is sent to the server, and the
+ buildinfo will be based on it. If the argument is not supplied, the
+ buildinfo is derived from the specfile which is currently on the source
+ repository server.
+
+ The returned data is XML and contains a list of the packages used in
+ building, their source, and the expanded BuildRequires.
+
+ The arguments REPOSITORY and ARCH are optional. They can be taken from
+ the first two columns of the 'osc repos' output. If not specified,
+ REPOSITORY defaults to the 'build_repositoy' config entry in your '.oscrc'
+ and ARCH defaults to your host architecture.
+
+ usage:
+ in a package working copy:
+ osc buildinfo [OPTS] REPOSITORY ARCH BUILD_DESCR
+ osc buildinfo [OPTS] REPOSITORY (ARCH = hostarch, BUILD_DESCR is detected automatically)
+ osc buildinfo [OPTS] ARCH (REPOSITORY = build_repository (config option), BUILD_DESCR is detected automatically)
+ osc buildinfo [OPTS] BUILD_DESCR (REPOSITORY = build_repository (config option), ARCH = hostarch)
+ osc buildinfo [OPTS] (REPOSITORY = build_repository (config option), ARCH = hostarch, BUILD_DESCR is detected automatically)
+ Note: if BUILD_DESCR does not exist locally the remote BUILD_DESCR is used
+
+ osc buildinfo [OPTS] PROJECT PACKAGE REPOSITORY ARCH [BUILD_DESCR]
+
+ ${cmd_option_list}
+ """
+ wd = os.curdir
+ args = slash_split(args)
+
+ project = package = repository = arch = build_descr = None
+ if len(args) <= 3:
+ if not is_package_dir('.'):
+ raise oscerr.WrongArgs('Incorrect number of arguments (Note: \'.\' is no package wc)')
+ project = store_read_project('.')
+ package = store_read_package('.')
+ repository, arch, build_descr = self.parse_repoarchdescr(args, ignore_descr=True)
+ elif len(args) == 4 or len(args) == 5:
+ project = args[0]
+ package = args[1]
+ repository = args[2]
+ arch = args[3]
+ if len(args) == 5:
+ build_descr = args[4]
+ else:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ apiurl = self.get_api_url()
+
+ build_descr_data = None
+ if not build_descr is None:
+ build_descr_data = open(build_descr, 'r').read()
+ if opts.prefer_pkgs and build_descr_data is None:
+ raise oscerr.WrongArgs('error: a build description is needed if \'--prefer-pkgs\' is used')
+ elif opts.prefer_pkgs:
+ from .build import get_prefer_pkgs
+ print('Scanning the following dirs for local packages: %s' % ', '.join(opts.prefer_pkgs))
+ prefer_pkgs, cpio = get_prefer_pkgs(opts.prefer_pkgs, arch, os.path.splitext(build_descr)[1])
+ cpio.add(os.path.basename(build_descr), build_descr_data)
+ build_descr_data = cpio.get()
+
+ print(''.join(get_buildinfo(apiurl,
+ project, package, repository, arch,
+ specfile=build_descr_data,
+ debug=opts.debug,
+ addlist=opts.extra_pkgs)))
+
+
+ def do_buildconfig(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build config
+
+ Shows the build configuration which is used in building a package.
+ This command is mostly used internally by the 'build' command.
+
+ The returned data is the project-wide build configuration in a format
+ which is directly readable by the build script. It contains RPM macros
+ and BuildRequires expansions, for example.
+
+ The argument REPOSITORY an be taken from the first column of the
+ 'osc repos' output.
+
+ usage:
+ osc buildconfig REPOSITORY (in pkg or prj dir)
+ osc buildconfig PROJECT REPOSITORY
+ ${cmd_option_list}
+ """
+
+ wd = os.curdir
+ args = slash_split(args)
+
+ if len(args) < 1 and (is_package_dir('.') or is_project_dir('.')):
+ self.print_repos(True)
+
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 1:
+ #FIXME: check if args[0] is really a repo and not a project, need a is_project() function for this
+ project = store_read_project(wd)
+ repository = args[0]
+ elif len(args) == 2:
+ project = args[0]
+ repository = args[1]
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ print(''.join(get_buildconfig(apiurl, project, repository)))
+
+
+ @cmdln.alias('repos')
+ @cmdln.alias('platforms')
+ def do_repositories(self, subcmd, opts, *args):
+ """${cmd_name}: shows repositories configured for a project.
+ It skips repositories by default which are disabled for a given package.
+
+ usage:
+ osc repos
+ osc repos [PROJECT] [PACKAGE]
+
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ project = None
+ package = None
+ disabled = None
+
+ if len(args) == 1:
+ project = args[0]
+ elif len(args) == 2:
+ project = args[0]
+ package = args[1]
+ elif len(args) == 0:
+ if is_package_dir('.'):
+ package = store_read_package('.')
+ project = store_read_project('.')
+ elif is_project_dir('.'):
+ project = store_read_project('.')
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ if project is None:
+ raise oscerr.WrongArgs('No project specified')
+
+ if package is not None:
+ disabled = show_package_disabled_repos(apiurl, project, package)
+
+ if subcmd == 'repos_only':
+ for repo in get_repositories_of_project(apiurl, project):
+ if (disabled is None) or ((disabled is not None) and (repo not in disabled)):
+ print(repo)
+ else:
+ data = []
+ for repo in get_repos_of_project(apiurl, project):
+ if (disabled is None) or ((disabled is not None) and (repo.name not in disabled)):
+ data += [repo.name, repo.arch]
+
+ for row in build_table(2, data, width=2):
+ print(row)
+
+
+ def parse_repoarchdescr(self, args, noinit = False, alternative_project = None, ignore_descr = False, vm_type = None):
+ """helper to parse the repo, arch and build description from args"""
+ import osc.build
+ import glob
+ arg_arch = arg_repository = arg_descr = None
+ if len(args) < 3:
+ # some magic, works only sometimes, but people seem to like it :/
+ all_archs = []
+ for mainarch in osc.build.can_also_build:
+ all_archs.append(mainarch)
+ for subarch in osc.build.can_also_build.get(mainarch):
+ all_archs.append(subarch)
+ for arg in args:
+ if arg.endswith('.spec') or arg.endswith('.dsc') or arg.endswith('.kiwi') or arg.endswith('.livebuild') or arg == 'PKGBUILD' or arg == 'build.collax':
+ arg_descr = arg
+ else:
+ if (arg == osc.build.hostarch or arg in all_archs) and arg_arch is None:
+ # it seems to be an architecture in general
+ arg_arch = arg
+ if not (arg in osc.build.can_also_build.get(osc.build.hostarch) or arg == osc.build.hostarch):
+ print("WARNING: native compile is not possible, an emulator must be configured!")
+ elif not arg_repository:
+ arg_repository = arg
+ else:
+# raise oscerr.WrongArgs('\'%s\' is neither a build description nor a supported arch' % arg)
+ # take it as arch (even though this is no supported arch) - hopefully, this invalid
+ # arch will be detected below
+ arg_arch = arg
+ else:
+ arg_repository, arg_arch, arg_descr = args
+
+ arg_arch = arg_arch or osc.build.hostarch
+
+ repositories = []
+ # store list of repos for potential offline use
+ repolistfile = os.path.join(os.getcwd(), osc.core.store, "_build_repositories")
+ if noinit:
+ repositories = Repo.fromfile(repolistfile)
+ else:
+ project = alternative_project or store_read_project('.')
+ apiurl = self.get_api_url()
+ repositories = list(get_repos_of_project(apiurl, project))
+ if not len(repositories):
+ raise oscerr.WrongArgs('no repositories defined for project \'%s\'' % project)
+ if alternative_project is None:
+ # only persist our own repos
+ Repo.tofile(repolistfile, repositories)
+
+ repo_names = sorted(set([r.name for r in repositories]))
+ if not arg_repository and repositories:
+ # XXX: we should avoid hardcoding repository names
+ # Use a default value from config, but just even if it's available
+ # unless try standard, or openSUSE_Factory, or openSUSE_Tumbleweed
+ arg_repository = repositories[-1].name
+ for repository in (conf.config['build_repository'], 'standard', 'openSUSE_Factory', 'openSUSE_Tumbleweed'):
+ if repository in repo_names:
+ arg_repository = repository
+ break
+
+ if not arg_repository:
+ raise oscerr.WrongArgs('please specify a repository')
+ if not noinit:
+ if not arg_repository in repo_names:
+ raise oscerr.WrongArgs('%s is not a valid repository, use one of: %s' % (arg_repository, ', '.join(repo_names)))
+ arches = [r.arch for r in repositories if r.name == arg_repository and r.arch]
+ if arches and not arg_arch in arches:
+ raise oscerr.WrongArgs('%s is not a valid arch for the repository %s, use one of: %s' % (arg_arch, arg_repository, ', '.join(arches)))
+
+ # can be implemented using
+ # reduce(lambda x, y: x + y, (glob.glob(x) for x in ('*.spec', '*.dsc', '*.kiwi')))
+ # but be a bit more readable :)
+ descr = glob.glob('*.spec') + glob.glob('*.dsc') + glob.glob('*.kiwi') + glob.glob('*.livebuild') + glob.glob('PKGBUILD') + glob.glob('build.collax')
+
+ # FIXME:
+ # * request repos from server and select by build type.
+ if not arg_descr and len(descr) == 1:
+ arg_descr = descr[0]
+ elif not arg_descr:
+ msg = None
+ if len(descr) > 1:
+ # guess/prefer build descrs like the following:
+ # <pac>-<repo>.<ext> > <pac>.<ext>
+ # no guessing for arch's PKGBUILD files (the backend does not do any guessing, too)
+ pac = os.path.basename(os.getcwd())
+ if is_package_dir(os.getcwd()):
+ pac = store_read_package(os.getcwd())
+ extensions = ['spec', 'dsc', 'kiwi', 'livebuild']
+ cands = [i for i in descr for ext in extensions if i == '%s-%s.%s' % (pac, arg_repository, ext)]
+ if len(cands) == 1:
+ arg_descr = cands[0]
+ else:
+ cands = [i for i in descr for ext in extensions if i == '%s.%s' % (pac, ext)]
+ if len(cands) == 1:
+ arg_descr = cands[0]
+ if not arg_descr:
+ msg = 'Multiple build description files found: %s' % ', '.join(descr)
+ elif not ignore_descr:
+ msg = 'Missing argument: build description (spec, dsc, kiwi or livebuild file)'
+ try:
+ p = Package('.')
+ if p.islink() and not p.isexpanded():
+ msg += ' (this package is not expanded - you might want to try osc up --expand)'
+ except:
+ pass
+ if msg:
+ raise oscerr.WrongArgs(msg)
+
+ return arg_repository, arg_arch, arg_descr
+
+
+ @cmdln.option('--clean', action='store_true',
+ help='Delete old build root before initializing it')
+ @cmdln.option('-o', '--offline', action='store_true',
+ help='Start with cached prjconf and packages without contacting the api server')
+ @cmdln.option('-l', '--preload', action='store_true',
+ help='Preload all files into the cache for offline operation')
+ @cmdln.option('--no-changelog', action='store_true',
+ help='don\'t update the package changelog from a changes file')
+ @cmdln.option('--rsync-src', metavar='RSYNCSRCPATH', dest='rsyncsrc',
+ help='Copy folder to buildroot after installing all RPMs. Use together with --rsync-dest. This is the path on the HOST filesystem e.g. /tmp/linux-kernel-tree. It defines RSYNCDONE 1 .')
+ @cmdln.option('--rsync-dest', metavar='RSYNCDESTPATH', dest='rsyncdest',
+ help='Copy folder to buildroot after installing all RPMs. Use together with --rsync-src. This is the path on the TARGET filesystem e.g. /usr/src/packages/BUILD/linux-2.6 .')
+ @cmdln.option('--overlay', metavar='OVERLAY',
+ help='Copy overlay filesystem to buildroot after installing all RPMs .')
+ @cmdln.option('--noinit', '--no-init', action='store_true',
+ help='Skip initialization of build root and start with build immediately.')
+ @cmdln.option('--nochecks', '--no-checks', action='store_true',
+ help='Do not run build checks on the resulting packages.')
+ @cmdln.option('--no-verify', '--noverify', action='store_true',
+ help='Skip signature verification of packages used for build. (Global config in .oscrc: no_verify)')
+ @cmdln.option('--noservice', '--no-service', action='store_true',
+ help='Skip run of local source services as specified in _service file.')
+ @cmdln.option('-p', '--prefer-pkgs', metavar='DIR', action='append',
+ help='Prefer packages from this directory when installing the build-root')
+ @cmdln.option('-k', '--keep-pkgs', metavar='DIR',
+ help='Save built packages into this directory')
+ @cmdln.option('-x', '--extra-pkgs', metavar='PAC', action='append',
+ help='Add this package when installing the build-root')
+ @cmdln.option('--root', metavar='ROOT',
+ help='Build in specified directory')
+ @cmdln.option('-j', '--jobs', metavar='N',
+ help='Compile with N jobs')
+ @cmdln.option('-t', '--threads', metavar='N',
+ help='Compile with N threads')
+ @cmdln.option('--icecream', metavar='N',
+ help='use N parallel build jobs with icecream')
+ @cmdln.option('--ccache', action='store_true',
+ help='use ccache to speed up rebuilds')
+ @cmdln.option('--with', metavar='X', dest='_with', action='append',
+ help='enable feature X for build')
+ @cmdln.option('--without', metavar='X', action='append',
+ help='disable feature X for build')
+ @cmdln.option('--define', metavar='\'X Y\'', action='append',
+ help='define macro X with value Y')
+ @cmdln.option('--userootforbuild', action='store_true',
+ help='Run build as root. The default is to build as '
+ 'unprivileged user. Note that a line "# norootforbuild" '
+ 'in the spec file will invalidate this option.')
+ @cmdln.option('--build-uid', metavar='uid:gid|"caller"',
+ help='specify the numeric uid:gid pair to assign to the '
+ 'unprivileged "abuild" user or use "caller" to use the current user uid:gid')
+ @cmdln.option('--local-package', action='store_true',
+ help='build a package which does not exist on the server')
+ @cmdln.option('--linksources', action='store_true',
+ help='use hard links instead of a deep copied source')
+ @cmdln.option('--vm-type', metavar='TYPE',
+ help='use VM type TYPE (e.g. kvm)')
+ @cmdln.option('--vm-telnet', metavar='TELNET',
+ help='Launch a telnet server inside of VM build')
+ @cmdln.option('--target', metavar='TARGET',
+ help='define target platform')
+ @cmdln.option('--alternative-project', metavar='PROJECT',
+ help='specify the build target project')
+ @cmdln.option('-d', '--debuginfo', action='store_true',
+ help='also build debuginfo sub-packages')
+ @cmdln.option('--disable-debuginfo', action='store_true',
+ help='disable build of debuginfo packages')
+ @cmdln.option('-b', '--baselibs', action='store_true',
+ help='Create -32bit/-64bit/-x86 rpms for other architectures')
+ @cmdln.option('--release', metavar='N',
+ help='set release number of the package to N')
+ @cmdln.option('--disable-cpio-bulk-download', action='store_true',
+ help='disable downloading packages as cpio archive from api')
+ @cmdln.option('--cpio-bulk-download', action='store_false',
+ dest='disable_cpio_bulk_download', help=SUPPRESS_HELP)
+ @cmdln.option('--download-api-only', action='store_true',
+ help='only fetch packages from the api')
+ @cmdln.option('--oldpackages', metavar='DIR',
+ help='take previous build from DIR (special values: _self, _link)')
+ @cmdln.option('--shell', action='store_true',
+ help=SUPPRESS_HELP)
+ @cmdln.option('--host', metavar='HOST',
+ help='perform the build on a remote server - user@server:~/remote/directory')
+ @cmdln.option('--trust-all-projects', action='store_true',
+ help='trust packages from all projects')
+ def do_build(self, subcmd, opts, *args):
+ """${cmd_name}: Build a package on your local machine
+
+ You need to call the command inside a package directory, which should be a
+ buildsystem checkout. (Local modifications are fine.)
+
+ The arguments REPOSITORY and ARCH can be taken from the first two columns
+ of the 'osc repos' output. BUILD_DESCR is either a RPM spec file, or a
+ Debian dsc file.
+
+ The command honours packagecachedir, build-root and build-uid
+ settings in .oscrc, if present. You may want to set su-wrapper = 'sudo'
+ in .oscrc, and configure sudo with option NOPASSWD for /usr/bin/build.
+
+ If neither --clean nor --noinit is given, build will reuse an existing
+ build-root again, removing unneeded packages and add missing ones. This
+ is usually the fastest option.
+
+ If the package doesn't exist on the server please use the --local-package
+ option.
+ If the project of the package doesn't exist on the server please use the
+ --alternative-project <alternative-project> option:
+ Example:
+ osc build [OPTS] --alternative-project openSUSE:10.3 standard i586 BUILD_DESCR
+
+ usage:
+ osc build [OPTS] REPOSITORY ARCH BUILD_DESCR
+ osc build [OPTS] REPOSITORY ARCH
+ osc build [OPTS] REPOSITORY (ARCH = hostarch, BUILD_DESCR is detected automatically)
+ osc build [OPTS] ARCH (REPOSITORY = build_repository (config option), BUILD_DESCR is detected automatically)
+ osc build [OPTS] BUILD_DESCR (REPOSITORY = build_repository (config option), ARCH = hostarch)
+ osc build [OPTS] (REPOSITORY = build_repository (config option), ARCH = hostarch, BUILD_DESCR is detected automatically)
+
+ # Note:
+ # Configuration can be overridden by envvars, e.g.
+ # OSC_SU_WRAPPER overrides the setting of su-wrapper.
+ # OSC_BUILD_ROOT overrides the setting of build-root.
+ # OSC_PACKAGECACHEDIR overrides the setting of packagecachedir.
+
+ ${cmd_option_list}
+ """
+
+ import osc.build
+
+ if which(conf.config['build-cmd']) is None:
+ print('Error: build (\'%s\') command not found' % conf.config['build-cmd'], file=sys.stderr)
+ print('Install the build package from http://download.opensuse.org/repositories/openSUSE:/Tools/', file=sys.stderr)
+ return 1
+
+ if opts.debuginfo and opts.disable_debuginfo:
+ raise oscerr.WrongOptions('osc: --debuginfo and --disable-debuginfo are mutual exclusive')
+
+ if len(args) > 3:
+ raise oscerr.WrongArgs('Too many arguments')
+
+ args = self.parse_repoarchdescr(args, opts.noinit or opts.offline, opts.alternative_project, False, opts.vm_type)
+
+ # check for source services
+ r = None
+ try:
+ if not opts.offline and not opts.noservice:
+ p = Package('.')
+ r = p.run_source_services(verbose=True)
+ except:
+ print("WARNING: package is not existing on server yet")
+ opts.local_package = True
+
+ if opts.offline or opts.local_package or r == None:
+ print("WARNING: source service from package or project will not be executed. This may not be the same build as on server!")
+ elif (conf.config['local_service_run'] and not opts.noservice) and not opts.noinit:
+ if r != 0:
+ print('Source service run failed!', file=sys.stderr)
+ sys.exit(1)
+ # that is currently unreadable on cli, we should not have a backtrace on standard errors:
+ #raise oscerr.ServiceRuntimeError('Service run failed: \'%s\'', r)
+
+ if conf.config['no_verify']:
+ opts.no_verify = True
+
+ if opts.keep_pkgs and not os.path.isdir(opts.keep_pkgs):
+ if os.path.exists(opts.keep_pkgs):
+ raise oscerr.WrongOptions('Preferred save location \'%s\' is not a directory' % opts.keep_pkgs)
+ else:
+ os.makedirs(opts.keep_pkgs)
+
+ if opts.prefer_pkgs:
+ for d in opts.prefer_pkgs:
+ if not os.path.isdir(d):
+ raise oscerr.WrongOptions('Preferred package location \'%s\' is not a directory' % d)
+
+ if opts.offline and opts.preload:
+ raise oscerr.WrongOptions('--offline and --preload are mutually exclusive')
+
+ print('Building %s for %s/%s' % (args[2], args[0], args[1]))
+ if not opts.host:
+ return osc.build.main(self.get_api_url(), opts, args)
+ else:
+ return self._do_rbuild(subcmd, opts, *args)
+
+ def _do_rbuild(self, subcmd, opts, *args):
+
+ # drop the --argument, value tuple from the list
+ def drop_arg2(lst, name):
+ if not name:
+ return lst
+ while name in lst:
+ i = lst.index(name)
+ lst.pop(i+1)
+ lst.pop(i)
+ return lst
+
+ # change the local directory to more suitable remote one in hostargs
+ # and perform the rsync to such location as well
+ def rsync_dirs_2host(hostargs, short_name, long_name, dirs):
+
+ drop_arg2(hostargs, short_name)
+ drop_arg2(hostargs, long_name)
+
+ for pdir in dirs:
+ # drop the last '/' from pdir name - this is because
+ # rsync foo remote:/bar create /bar/foo on remote machine
+ # rsync foo/ remote:/bar copy the content of foo in the /bar
+ if pdir[-1:] == os.path.sep:
+ pdir = pdir[:-1]
+
+ hostprefer = os.path.join(
+ hostpath,
+ basename,
+ "%s__" % (long_name.replace('-', '_')),
+ os.path.basename(os.path.abspath(pdir)))
+ hostargs.append(long_name)
+ hostargs.append(hostprefer)
+
+ rsync_prefer_cmd = ['rsync', '-az', '--delete', '-e', 'ssh',
+ pdir,
+ "%s:%s" % (hostname, os.path.dirname(hostprefer))]
+ print('Run: %s' % " ".join(rsync_prefer_cmd))
+ ret = run_external(rsync_prefer_cmd[0], *rsync_prefer_cmd[1:])
+ if ret != 0:
+ return ret
+
+ return 0
+
+
+ cwd = os.getcwd()
+ basename = os.path.basename(cwd)
+ if not ':' in opts.host:
+ hostname = opts.host
+ hostpath = "~/"
+ else:
+ hostname, hostpath = opts.host.split(':', 1)
+
+ # arguments for build: use all arguments behind build and drop --host 'HOST'
+ hostargs = sys.argv[sys.argv.index(subcmd)+1:]
+ drop_arg2(hostargs, '--host')
+
+ # global arguments: use first '-' up to subcmd
+ gi = 0
+ for i, a in enumerate(sys.argv):
+ if a == subcmd:
+ break
+ if a[0] == '-':
+ gi = i
+ break
+
+ if gi:
+ hostglobalargs = sys.argv[gi : sys.argv.index(subcmd)+1]
+ else:
+ hostglobalargs = (subcmd, )
+
+ # keep-pkgs
+ hostkeep = None
+ if opts.keep_pkgs:
+ drop_arg2(hostargs, '-k')
+ drop_arg2(hostargs, '--keep-pkgs')
+ hostkeep = os.path.join(
+ hostpath,
+ basename,
+ "__keep_pkgs__",
+ "") # <--- this adds last '/', thus triggers correct rsync behavior
+ hostargs.append('--keep-pkgs')
+ hostargs.append(hostkeep)
+
+ ### run all commands ###
+ # 1.) rsync sources
+ rsync_source_cmd = ['rsync', '-az', '--delete', '-e', 'ssh', cwd, "%s:%s" % (hostname, hostpath)]
+ print('Run: %s' % " ".join(rsync_source_cmd))
+ ret = run_external(rsync_source_cmd[0], *rsync_source_cmd[1:])
+ if ret != 0:
+ return ret
+
+ # 2.) rsync prefer-pkgs dirs, overlay and rsyns-src
+ if opts.prefer_pkgs:
+ ret = rsync_dirs_2host(hostargs, '-p', '--prefer-pkgs', opts.prefer_pkgs)
+ if ret != 0:
+ return ret
+
+ for arg, long_name in ((opts.rsyncsrc, '--rsync-src'), (opts.overlay, '--overlay')):
+ if not arg:
+ continue
+ ret = rsync_dirs_2host(hostargs, None, long_name, (arg, ))
+ if ret != 0:
+ return ret
+
+ # 3.) call osc build
+ osc_cmd = "osc"
+ for var in ('OSC_SU_WRAPPER', 'OSC_BUILD_ROOT', 'OSC_PACKAGECACHEDIR'):
+ if os.getenv(var):
+ osc_cmd = "%s=%s %s" % (var, os.getenv(var), osc_cmd)
+
+ ssh_cmd = \
+ ['ssh', '-t', hostname,
+ "cd %(remote_dir)s; %(osc_cmd)s %(global_args)s %(local_args)s" % dict(
+ remote_dir = os.path.join(hostpath, basename),
+ osc_cmd = osc_cmd,
+ global_args = " ".join(hostglobalargs),
+ local_args = " ".join(hostargs))
+ ]
+ print('Run: %s' % " ".join(ssh_cmd))
+ build_ret = run_external(ssh_cmd[0], *ssh_cmd[1:])
+ if build_ret != 0:
+ return build_ret
+
+ # 4.) get keep-pkgs back
+ if opts.keep_pkgs:
+ ret = rsync_keep_cmd = ['rsync', '-az', '-e', 'ssh', "%s:%s" % (hostname, hostkeep), opts.keep_pkgs]
+ print('Run: %s' % " ".join(rsync_keep_cmd))
+ ret = run_external(rsync_keep_cmd[0], *rsync_keep_cmd[1:])
+ if ret != 0:
+ return ret
+
+ return build_ret
+
+
+ @cmdln.option('--local-package', action='store_true',
+ help='package doesn\'t exist on the server')
+ @cmdln.option('--alternative-project', metavar='PROJECT',
+ help='specify the used build target project')
+ @cmdln.option('--noinit', '--no-init', action='store_true',
+ help='do not guess/verify specified repository')
+ @cmdln.option('-r', '--login-as-root', action='store_true',
+ help='login as root instead of abuild')
+ @cmdln.option('--root', metavar='ROOT',
+ help='Path to the buildroot')
+ @cmdln.option('-o', '--offline', action='store_true',
+ help='Use cached data without contacting the api server')
+ def do_chroot(self, subcmd, opts, *args):
+ """${cmd_name}: opens a shell inside of the build root
+
+ chroot into the build root for the given repository, arch and build description
+ (NOTE: this command does not work if a VM is used)
+
+ usage:
+ osc chroot [OPTS] REPOSITORY ARCH BUILD_DESCR
+ osc chroot [OPTS] REPOSITORY (ARCH = hostarch, BUILD_DESCR is detected automatically)
+ osc chroot [OPTS] ARCH (REPOSITORY = build_repository (config option), BUILD_DESCR is detected automatically)
+ osc chroot [OPTS] BUILD_DESCR (REPOSITORY = build_repository (config option), ARCH = hostarch)
+ osc chroot [OPTS] (REPOSITORY = build_repository (config option), ARCH = hostarch, BUILD_DESCR is detected automatically)
+ ${cmd_option_list}
+ """
+ if len(args) > 3:
+ raise oscerr.WrongArgs('Too many arguments')
+ if conf.config['build-type'] and conf.config['build-type'] != "lxc":
+ print('Not implemented for VMs', file=sys.stderr)
+ sys.exit(1)
+
+ user = 'abuild'
+ if opts.login_as_root:
+ user = 'root'
+ buildroot = opts.root
+ if buildroot is None:
+ repository, arch, descr = self.parse_repoarchdescr(args, opts.noinit or opts.offline, opts.alternative_project)
+ project = opts.alternative_project or store_read_project('.')
+ if opts.local_package:
+ package = os.path.splitext(descr)[0]
+ else:
+ package = store_read_package('.')
+ apihost = urlsplit(self.get_api_url())[1]
+ if buildroot is None:
+ buildroot = os.environ.get('OSC_BUILD_ROOT', conf.config['build-root']) \
+ % {'repo': repository, 'arch': arch, 'project': project, 'package': package, 'apihost': apihost}
+ if not os.path.isdir(buildroot):
+ raise oscerr.OscIOError(None, '\'%s\' is not a directory' % buildroot)
+
+ suwrapper = os.environ.get('OSC_SU_WRAPPER', conf.config['su-wrapper'])
+ sucmd = suwrapper.split()[0]
+ suargs = ' '.join(suwrapper.split()[1:])
+ if suwrapper.startswith('su '):
+ cmd = [sucmd, '%s chroot "%s" su - %s' % (suargs, buildroot, user)]
+ else:
+ cmd = [sucmd, 'chroot', buildroot, 'su', '-', user]
+ if suargs:
+ cmd[1:1] = suargs.split()
+ print('running: %s' % ' '.join(cmd))
+ os.execvp(sucmd, cmd)
+
+
+ @cmdln.option('', '--csv', action='store_true',
+ help='generate output in CSV (separated by |)')
+ @cmdln.option('-l', '--limit', metavar='limit',
+ help='for setting the number of results')
+ @cmdln.alias('buildhist')
+ def do_buildhistory(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the build history of a package
+
+ The arguments REPOSITORY and ARCH can be taken from the first two columns
+ of the 'osc repos' output.
+
+ usage:
+ osc buildhist REPOSITORY ARCHITECTURE
+ osc buildhist PROJECT PACKAGE REPOSITORY ARCHITECTURE
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ if len(args) < 2 and is_package_dir('.'):
+ self.print_repos()
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 4:
+ project = args[0]
+ package = args[1]
+ repository = args[2]
+ arch = args[3]
+ elif len(args) == 2:
+ wd = os.curdir
+ package = store_read_package(wd)
+ project = store_read_project(wd)
+ repository = args[0]
+ arch = args[1]
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ format = 'text'
+ if opts.csv:
+ format = 'csv'
+
+ print('\n'.join(get_buildhistory(apiurl, project, package, repository, arch, format, opts.limit)))
+
+ @cmdln.option('', '--csv', action='store_true',
+ help='generate output in CSV (separated by |)')
+ @cmdln.option('-l', '--limit', metavar='limit',
+ help='for setting the number of results')
+ @cmdln.alias('jobhist')
+ def do_jobhistory(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the job history of a project
+
+ The arguments REPOSITORY and ARCH can be taken from the first two columns
+ of the 'osc repos' output.
+
+ usage:
+ osc jobhist REPOSITORY ARCHITECTURE (in project dir)
+ osc jobhist PROJECT [PACKAGE] REPOSITORY ARCHITECTURE
+ ${cmd_option_list}
+ """
+ wd = os.curdir
+ args = slash_split(args)
+
+ if len(args) < 2 and (is_project_dir('.') or is_package_dir('.')):
+ self.print_repos()
+
+ apiurl = self.get_api_url()
+
+ if len(args) == 4:
+ project = args[0]
+ package = args[1]
+ repository = args[2]
+ arch = args[3]
+ elif len(args) == 3:
+ project = args[0]
+ package = None # skipped = prj
+ repository = args[1]
+ arch = args[2]
+ elif len(args) == 2:
+ package = None
+ try:
+ package = store_read_package(wd)
+ except:
+ pass
+ project = store_read_project(wd)
+ repository = args[0]
+ arch = args[1]
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments')
+
+ format = 'text'
+ if opts.csv:
+ format = 'csv'
+
+ print_jobhistory(apiurl, project, package, repository, arch, format, opts.limit)
+
+ @cmdln.hide(1)
+ def do_rlog(self, subcmd, opts, *args):
+ print("Command rlog is obsolete. Please use 'osc log'")
+ sys.exit(1)
+
+
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='show log of the specified revision')
+ @cmdln.option('', '--csv', action='store_true',
+ help='generate output in CSV (separated by |)')
+ @cmdln.option('', '--xml', action='store_true',
+ help='generate output in XML')
+ @cmdln.option('-D', '--deleted', action='store_true',
+ help='work on deleted package')
+ @cmdln.option('-M', '--meta', action='store_true',
+ help='checkout out meta data instead of sources' )
+ def do_log(self, subcmd, opts, *args):
+ """${cmd_name}: Shows the commit log of a package
+
+ Usage:
+ osc log (inside working copy)
+ osc log remote_project [remote_package]
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ apiurl = self.get_api_url()
+
+ if len(args) == 0:
+ wd = os.curdir
+ if is_project_dir(wd) or is_package_dir(wd):
+ project = store_read_project(wd)
+ if is_project_dir(wd):
+ package = "_project"
+ else:
+ package = store_read_package(wd)
+ else:
+ raise oscerr.NoWorkingCopy("Error: \"%s\" is not an osc working copy." % os.path.abspath(wd))
+ elif len(args) < 1:
+ raise oscerr.WrongArgs('Too few arguments (required none or two)')
+ elif len(args) > 2:
+ raise oscerr.WrongArgs('Too many arguments (required none or two)')
+ elif len(args) == 1:
+ project = args[0]
+ package = "_project"
+ else:
+ project = args[0]
+ package = args[1]
+
+ rev, rev_upper = parseRevisionOption(opts.revision)
+ if rev and not checkRevision(project, package, rev, apiurl, opts.meta):
+ print('Revision \'%s\' does not exist' % rev, file=sys.stderr)
+ sys.exit(1)
+
+ format = 'text'
+ if opts.csv:
+ format = 'csv'
+ if opts.xml:
+ format = 'xml'
+
+ log = '\n'.join(get_commitlog(apiurl, project, package, rev, format, opts.meta, opts.deleted, rev_upper))
+ run_pager(log)
+
+ def do_service(self, subcmd, opts, *args):
+ """${cmd_name}: Handle source services
+
+ Source services can be used to modify sources like downloading files,
+ verify files, generating files or modify existing files.
+
+ usage:
+ osc service COMMAND (inside working copy)
+ osc service run [SOURCE_SERVICE]
+ osc service localrun
+ osc service disabledrun
+ osc service remoterun [PROJECT PACKAGE]
+ osc service merge [PROJECT PACKAGE]
+ osc service wait [PROJECT PACKAGE]
+
+ COMMAND can be:
+ run r run defined services locally, it takes an optional parameter to run only a
+ specified source service. In case parameters exist for this one in _service file
+ they are used.
+ localrun lr run services locally and store files as local created
+ disabledrun dr run disabled or server side only services locally and store files as local created
+ remoterun rr trigger a re-run on the server side
+ merge commits all server side generated files and drops the _service definition
+ wait waits until the service finishes and returns with an error if it failed
+
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+ project = package = singleservice = mode = None
+ apiurl = self.get_api_url()
+
+ if len(args) < 1:
+ raise oscerr.WrongArgs('No command given.')
+ elif len(args) < 3:
+ if is_package_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ else:
+ raise oscerr.WrongArgs('Too few arguments.')
+ if len(args) == 2:
+ singleservice = args[1]
+ elif len(args) == 3 and args[0] in ('remoterun', 'rr', 'merge', 'wait'):
+ project = args[1]
+ package = args[2]
+ else:
+ raise oscerr.WrongArgs('Too many arguments.')
+
+ command = args[0]
+
+ if not (command in ( 'run', 'localrun', 'disabledrun', 'remoterun', 'lr', 'dr', 'r', 'rr', 'merge', 'wait' )):
+ raise oscerr.WrongArgs('Wrong command given.')
+
+ if command == "remoterun" or command == "rr":
+ print(runservice(apiurl, project, package))
+ return
+
+ if command == "wait":
+ print(waitservice(apiurl, project, package))
+ return
+
+ if command == "merge":
+ print(mergeservice(apiurl, project, package))
+ return
+
+ if command in ('run', 'localrun', 'disabledrun', 'lr', 'dr', 'r'):
+ if not is_package_dir(os.curdir):
+ raise oscerr.WrongArgs('Local directory is no package')
+ p = Package(".")
+ if command == "localrun" or command == "lr":
+ mode = "local"
+ elif command == "disabledrun" or command == "dr":
+ mode = "disabled"
+
+ return p.run_source_services(mode, singleservice)
+
+ @cmdln.option('-a', '--arch', metavar='ARCH',
+ help='trigger rebuilds for a specific architecture')
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='trigger rebuilds for a specific repository')
+ @cmdln.option('-f', '--failed', action='store_true',
+ help='rebuild all failed packages')
+ @cmdln.option('--all', action='store_true',
+ help='Rebuild all packages of entire project')
+ @cmdln.alias('rebuildpac')
+ def do_rebuild(self, subcmd, opts, *args):
+ """${cmd_name}: Trigger package rebuilds
+
+ Note that it is normally NOT needed to kick off rebuilds like this, because
+ they principally happen in a fully automatic way, triggered by source
+ check-ins. In particular, the order in which packages are built is handled
+ by the build service.
+
+ The arguments REPOSITORY and ARCH can be taken from the first two columns
+ of the 'osc repos' output.
+
+ usage:
+ osc rebuild [PROJECT [PACKAGE [REPOSITORY [ARCH]]]]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ package = repo = arch = code = None
+ apiurl = self.get_api_url()
+
+ if opts.repo:
+ repo = opts.repo
+
+ if opts.arch:
+ arch = opts.arch
+
+ if len(args) < 1:
+ if is_package_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ apiurl = store_read_apiurl(os.curdir)
+ elif is_project_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ apiurl = store_read_apiurl(os.curdir)
+ else:
+ raise oscerr.WrongArgs('Too few arguments.')
+ else:
+ project = args[0]
+ if len(args) > 1:
+ package = args[1]
+
+ if len(args) > 2:
+ repo = args[2]
+ if len(args) > 3:
+ arch = args[3]
+
+ if opts.failed:
+ code = 'failed'
+
+ if not (opts.all or package or repo or arch or code):
+ raise oscerr.WrongOptions('No option has been provided. If you want to rebuild all packages of the entire project, use --all option.')
+
+ print(rebuild(apiurl, project, package, repo, arch, code))
+
+
+ def do_info(self, subcmd, opts, *args):
+ """${cmd_name}: Print information about a working copy
+
+ Print information about each ARG (default: '.')
+ ARG is a working-copy path.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ args = parseargs(args)
+ pacs = findpacs(args)
+
+ for p in pacs:
+ print(p.info())
+
+
+ @cmdln.option('-a', '--arch', metavar='ARCH',
+ help='Restart builds for a specific architecture')
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='Restart builds for a specific repository')
+ @cmdln.option('--all', action='store_true',
+ help='Restart all running builds of entire project')
+ @cmdln.alias('abortbuild')
+ def do_restartbuild(self, subcmd, opts, *args):
+ """${cmd_name}: Restart the build of a certain project or package
+
+ usage:
+ osc restartbuild [PROJECT [PACKAGE [REPOSITORY [ARCH]]]]
+ ${cmd_option_list}
+ """
+ args = slash_split(args)
+
+ package = repo = arch = code = None
+ apiurl = self.get_api_url()
+
+ if opts.repo:
+ repo = opts.repo
+
+ if opts.arch:
+ arch = opts.arch
+
+ if len(args) < 1:
+ if is_package_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ apiurl = store_read_apiurl(os.curdir)
+ elif is_project_dir(os.curdir):
+ project = store_read_project(os.curdir)
+ apiurl = store_read_apiurl(os.curdir)
+ else:
+ raise oscerr.WrongArgs('Too few arguments.')
+ else:
+ project = args[0]
+ if len(args) > 1:
+ package = args[1]
+
+ if len(args) > 2:
+ repo = args[2]
+ if len(args) > 3:
+ arch = args[3]
+
+ if not (opts.all or package or repo or arch):
+ raise oscerr.WrongOptions('No option has been provided. If you want to restart all packages of the entire project, use --all option.')
+
+ print(cmdbuild(apiurl, subcmd, project, package, opts.arch, opts.repo))
+
+
+ @cmdln.option('-a', '--arch', metavar='ARCH',
+ help='Delete all binary packages for a specific architecture')
+ @cmdln.option('-r', '--repo', metavar='REPO',
+ help='Delete all binary packages for a specific repository')
+ @cmdln.option('--build-disabled', action='store_true',
+ help='Delete all binaries of packages for which the build is disabled')
+ @cmdln.option('--build-failed', action='store_true',
+ help='Delete all binaries of packages for which the build failed')
+ @cmdln.option('--broken', action='store_true',
+ help='Delete all binaries of packages for which the package source is bad')
+ @cmdln.option('--unresolvable', action='store_true',
+ help='Delete all binaries of packages which have dependency errors')
+ @cmdln.option('--all', action='store_true',
+ help='Delete all binaries regardless of the package status (previously default)')
+ def do_wipebinaries(self, subcmd, opts, *args):
+ """${cmd_name}: Delete all binary packages of a certain project/package
+
+ With the optional argument <package> you can specify a certain package
+ otherwise all binary packages in the project will be deleted.
+
+ usage:
+ osc wipebinaries OPTS # works in checked out project dir
+ osc wipebinaries OPTS PROJECT [PACKAGE]
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ package = project = None
+ apiurl = self.get_api_url()
+
+ # try to get project and package from checked out dirs
+ if len(args) < 1:
+ if is_project_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ if is_package_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ if project is None:
+ raise oscerr.WrongArgs('Missing <project> argument.')
+ if len(args) > 2:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ # respect given project and package
+ if len(args) >= 1:
+ project = args[0]
+
+ if len(args) == 2:
+ package = args[1]
+
+ codes = []
+ if opts.build_disabled:
+ codes.append('disabled')
+ if opts.build_failed:
+ codes.append('failed')
+ if opts.broken:
+ codes.append('broken')
+ if opts.unresolvable:
+ codes.append('unresolvable')
+ if opts.all or opts.repo or opts.arch:
+ codes.append(None)
+
+ if len(codes) == 0:
+ raise oscerr.WrongOptions('No option has been provided. If you want to delete all binaries, use --all option.')
+
+ # make a new request for each code= parameter
+ for code in codes:
+ print(wipebinaries(apiurl, project, package, opts.arch, opts.repo, code))
+
+
+ @cmdln.option('-q', '--quiet', action='store_true',
+ help='do not show downloading progress')
+ @cmdln.option('-d', '--destdir', default='./binaries', metavar='DIR',
+ help='destination directory')
+ @cmdln.option('--sources', action="store_true",
+ help='also fetch source packages')
+ @cmdln.option('--debug', action="store_true",
+ help='also fetch debug packages')
+ def do_getbinaries(self, subcmd, opts, *args):
+ """${cmd_name}: Download binaries to a local directory
+
+ This command downloads packages directly from the api server.
+ Thus, it directly accesses the packages that are used for building
+ others even when they are not "published" yet.
+
+ usage:
+ osc getbinaries REPOSITORY # works in checked out project/package (check out all archs in subdirs)
+ osc getbinaries REPOSITORY ARCHITECTURE # works in checked out project/package
+ osc getbinaries PROJECT PACKAGE REPOSITORY ARCHITECTURE
+ osc getbinaries PROJECT PACKAGE REPOSITORY ARCHITECTURE FILE
+ ${cmd_option_list}
+ """
+
+ args = slash_split(args)
+
+ apiurl = self.get_api_url()
+ project = None
+ package = None
+ binary = None
+
+ if len(args) < 1 and is_package_dir('.'):
+ self.print_repos()
+
+ architecture = None
+ if len(args) == 4 or len(args) == 5:
+ project = args[0]
+ package = args[1]
+ repository = args[2]
+ architecture = args[3]
+ if len(args) == 5:
+ binary = args[4]
+ elif len(args) >= 1 and len(args) <= 2:
+ if is_package_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ package = store_read_package(os.curdir)
+ elif is_project_dir(os.getcwd()):
+ project = store_read_project(os.curdir)
+ else:
+ raise oscerr.WrongArgs('Missing arguments: either specify <project> and ' \
+ '<package> or move to a project or package working copy')
+ repository = args[0]
+ if len(args) == 2:
+ architecture = args[1]
+ else:
+ raise oscerr.WrongArgs('Need either 1, 2 or 4 arguments')
+
+ repos = list(get_repos_of_project(apiurl, project))
+ if not [i for i in repos if repository == i.name]:
+ self.print_repos(exc_msg='Invalid repository \'%s\'' % repository)
+
+ arches = [architecture]
+ if architecture is None:
+ arches = [i.arch for i in repos if repository == i.name]
+
+ if package is None:
+ package = meta_get_packagelist(apiurl, project)
+ else:
+ package = [package]
+
+ # Set binary target directory and create if not existing
+ target_dir = os.path.normpath(opts.destdir)
+ if not os.path.isdir(target_dir):
+ print('Creating %s' % target_dir)
+ os.makedirs(target_dir, 0o755)
+
+ for arch in arches:
+ for pac in package:
+ binaries = get_binarylist(apiurl, project, repository, arch,
+ package=pac, verbose=True)
+ if not binaries:
+ print('no binaries found: Either the package %s ' \
+ 'does not exist or no binaries have been built.' % pac, file=sys.stderr)
+ continue
+
+ for i in binaries:
+ if binary != None and binary != i.name:
+ continue
+ # skip source rpms
+ if not opts.sources and (i.name.endswith('src.rpm') or i.name.endswith('sdeb')):
+ continue
+ if not opts.debug:
+ if i.name.find('-debuginfo-') >= 0:
+ continue
+ if i.name.find('-debugsource-') >= 0:
+ continue
+ fname = '%s/%s' % (target_dir, i.name)
+ if os.path.exists(fname):
+ st = os.stat(fname)
+ if st.st_mtime == i.mtime and st.st_size == i.size:
+ continue
+ get_binary_file(apiurl,
+ project,
+ repository, arch,
+ i.name,
+ package = pac,
+ target_filename = fname,
+ target_mtime = i.mtime,
+ progress_meter = not opts.quiet)
+
+
+ @cmdln.option('-b', '--bugowner', action='store_true',
+ help='restrict listing to items where the user is bugowner')
+ @cmdln.option('-m', '--maintainer', action='store_true',
+ help='restrict listing to items where the user is maintainer')
+ @cmdln.option('-a', '--all', action='store_true',
+ help='all involvements')
+ @cmdln.option('-U', '--user', metavar='USER',
+ help='search for USER instead of yourself')
+ @cmdln.option('--exclude-project', action='append',
+ help='exclude requests for specified project')
+ @cmdln.option('-v', '--verbose', action='store_true',
+ help='verbose listing')
+ @cmdln.option('--maintained', action='store_true',
+ help='limit search results to packages with maintained attribute set.')
+ def do_my(self, subcmd, opts, *args):
+ """${cmd_name}: show waiting work, packages, projects or requests involving yourself
+
+ Examples:
+ # list all open tasks for me
+ osc ${cmd_name} [work]
+ # list packages where I am bugowner
+ osc ${cmd_name} pkg -b
+ # list projects where I am maintainer
+ osc ${cmd_name} prj -m
+ # list request for all my projects and packages
+ osc ${cmd_name} rq
+ # list requests, excluding project 'foo' and 'bar'
+ osc ${cmd_name} rq --exclude-project foo,bar
+ # list requests I made
+ osc ${cmd_name} sr
+
+ ${cmd_usage}
+ where TYPE is one of requests, submitrequests,
+ projects or packages (rq, sr, prj or pkg)
+
+ ${cmd_option_list}
+ """
+
+ # TODO: please clarify the difference between sr and rq.
+ # My first implementeation was to make no difference between requests FROM one
+ # of my projects and TO one of my projects. The current implementation appears to make this difference.
+ # The usage above indicates, that sr would be a subset of rq, which is no the case with my tests.
+ # jw.
+
+ args_rq = ('requests', 'request', 'req', 'rq', 'work')
+ args_sr = ('submitrequests', 'submitrequest', 'submitreq', 'submit', 'sr')
+ args_prj = ('projects', 'project', 'projs', 'proj', 'prj')
+ args_pkg = ('packages', 'package', 'pack', 'pkgs', 'pkg')
+ args_patchinfos = ('patchinfos', 'work')
+
+ if opts.bugowner and opts.maintainer:
+ raise oscerr.WrongOptions('Sorry, \'--bugowner\' and \'maintainer\' are mutually exclusive')
+ elif opts.all and (opts.bugowner or opts.maintainer):
+ raise oscerr.WrongOptions('Sorry, \'--all\' and \'--bugowner\' or \'--maintainer\' are mutually exclusive')
+
+ apiurl = self.get_api_url()
+
+ exclude_projects = []
+ for i in opts.exclude_project or []:
+ prj = i.split(',')
+ if len(prj) == 1:
+ exclude_projects.append(i)
+ else:
+ exclude_projects.extend(prj)
+ if not opts.user:
+ user = conf.get_apiurl_usr(apiurl)
+ else:
+ user = opts.user
+
+ what = {'project': '', 'package': ''}
+ type = "work"
+ if len(args) > 0:
+ type = args[0]
+
+ list_patchinfos = list_requests = False
+ if type in args_patchinfos:
+ list_patchinfos = True
+ if type in args_rq:
+ list_requests = True
+ elif type in args_prj:
+ what = {'project': ''}
+ elif type in args_sr:
+ requests = get_request_collection(apiurl, 'creator', req_who=user)
+ for r in sorted(requests):
+ print(r.list_view(), '\n')
+ return
+ elif not type in args_pkg:
+ raise oscerr.WrongArgs("invalid type %s" % type)
+
+ role_filter = ''
+ if opts.maintainer:
+ role_filter = 'maintainer'
+ elif opts.bugowner:
+ role_filter = 'bugowner'
+ elif list_requests:
+ role_filter = 'maintainer'
+ if opts.all:
+ role_filter = ''
+
+ if list_patchinfos:
+ u = makeurl(apiurl, ['/search/package'], {
+ 'match' : "([kind='patchinfo' and issue/[@state='OPEN' and owner/@login='%s']])" % user
+ })
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ if root.findall('package'):
+ print("Patchinfos with open bugs assigned to you:\n")
+ for node in root.findall('package'):
+ project = node.get('project')
+ package = node.get('name')
+ print(project, "/", package, '\n')
+ p = makeurl(apiurl, ['source', project, package], { 'view': 'issues' })
+ fp = http_GET(p)
+ issues = ET.parse(fp).findall('issue')
+ for issue in issues:
+ if issue.find('state') == None or issue.find('state').text != "OPEN":
+ continue
+ if issue.find('owner') == None or issue.find('owner').find('login').text != user:
+ continue
+ print(" #", issue.find('label').text, ': ', end=' ')
+ desc = issue.find('summary')
+ if desc != None:
+ print(desc.text)
+ else:
+ print("\n")
+ print("")
+
+ if list_requests:
+ # try api side search as supported since OBS 2.2
+ try:
+ requests = []
+ # open reviews
+ u = makeurl(apiurl, ['request'], {
+ 'view': 'collection',
+ 'states': 'review',
+ 'reviewstates': 'new',
+ 'roles': 'reviewer',
+ 'user': user,
+ })
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ if root.findall('request'):
+ print("Requests which request a review by you:\n")
+ for node in root.findall('request'):
+ r = Request()
+ r.read(node)
+ print(r.list_view(), '\n')
+ print("")
+ # open requests
+ u = makeurl(apiurl, ['request'], {
+ 'view': 'collection',
+ 'states': 'new',
+ 'roles': 'maintainer',
+ 'user': user,
+ })
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ if root.findall('request'):
+ print("Requests for your packages:\n")
+ for node in root.findall('request'):
+ r = Request()
+ r.read(node)
+ print(r.list_view(), '\n')
+ print("")
+ # declined requests submitted by me
+ u = makeurl(apiurl, ['request'], {
+ 'view': 'collection',
+ 'states': 'declined',
+ 'roles': 'creator',
+ 'user': user,
+ })
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ if root.findall('request'):
+ print("Declined requests created by you (revoke, reopen or supersede):\n")
+ for node in root.findall('request'):
+ r = Request()
+ r.read(node)
+ print(r.list_view(), '\n')
+ print("")
+ return
+ except HTTPError as e:
+ if e.code == 400:
+ # skip it ... try again with old style below
+ pass
+
+ res = get_user_projpkgs(apiurl, user, role_filter, exclude_projects,
+ 'project' in what, 'package' in what,
+ opts.maintained, opts.verbose)
+
+ # map of project =>[list of packages]
+ # if list of packages is empty user is maintainer of the whole project
+ request_todo = {}
+
+ roles = {}
+ if len(what.keys()) == 2:
+ for i in res.get('project_id', res.get('project', {})).findall('project'):
+ request_todo[i.get('name')] = []
+ roles[i.get('name')] = [p.get('role') for p in i.findall('person') if p.get('userid') == user]
+ for i in res.get('package_id', res.get('package', {})).findall('package'):
+ prj = i.get('project')
+ roles['/'.join([prj, i.get('name')])] = [p.get('role') for p in i.findall('person') if p.get('userid') == user]
+ if not prj in request_todo or request_todo[prj] != []:
+ request_todo.setdefault(prj, []).append(i.get('name'))
+ else:
+ for i in res.get('project_id', res.get('project', {})).findall('project'):
+ roles[i.get('name')] = [p.get('role') for p in i.findall('person') if p.get('userid') == user]
+
+ if list_requests:
+ # old style, only for OBS 2.1 and before. Should not be used, since it is slow and incomplete
+ requests = get_user_projpkgs_request_list(apiurl, user, projpkgs=request_todo)
+ for r in sorted(requests):
+ print(r.list_view(), '\n')
+ if not len(requests):
+ print(" -> try also 'osc my sr' to see more.")
+ else:
+ for i in sorted(roles.keys()):
+ out = '%s' % i
+ prjpac = i.split('/')
+ if type in args_pkg and len(prjpac) == 1 and not opts.verbose:
+ continue
+ if opts.verbose:
+ out = '%s (%s)' % (i, ', '.join(sorted(roles[i])))
+ if len(prjpac) == 2:
+ out = ' %s (%s)' % (prjpac[1], ', '.join(sorted(roles[i])))
+ print(out)
+
+
+ @cmdln.option('--repos-baseurl', action='store_true',
+ help='show base URLs of download repositories')
+ @cmdln.option('-e', '--exact', action='store_true',
+ help='show only exact matches, this is default now')
+ @cmdln.option('-s', '--substring', action='store_true',
+ help='Show also results where the search term is a sub string, slower search')
+ @cmdln.option('--package', action='store_true',
+ help='search for a package')
+ @cmdln.option('--project', action='store_true',
+ help='search for a project')
+ @cmdln.option('--title', action='store_true',
+ help='search for matches in the \'title\' element')
+ @cmdln.option('--description', action='store_true',
+ help='search for matches in the \'description\' element')
+ @cmdln.option('-a', '--limit-to-attribute', metavar='ATTRIBUTE',
+ help='match only when given attribute exists in meta data')
+ @cmdln.option('-v', '--verbose', action='store_true',
+ help='show more information')
+ @cmdln.option('-V', '--version', action='store_true',
+ help='show package version, revision, and srcmd5. CAUTION: This is slow and unreliable')
+ @cmdln.option('-i', '--involved', action='store_true',
+ help='show projects/packages where given person (or myself) is involved as bugowner or maintainer [[{group|person}/]<name>] default: person')
+ @cmdln.option('-b', '--bugowner', action='store_true',
+ help='as -i, but only bugowner')
+ @cmdln.option('-m', '--maintainer', action='store_true',
+ help='as -i, but only maintainer')
+ @cmdln.option('--maintained', action='store_true',
+ help='OBSOLETE: please use maintained command instead.')
+ @cmdln.option('-M', '--mine', action='store_true',
+ help='shorthand for --bugowner --package')
+ @cmdln.option('--csv', action='store_true',
+ help='generate output in CSV (separated by |)')
+ @cmdln.option('--binary', action='store_true',
+ help='search binary packages')
+ @cmdln.option('-B', '--baseproject', metavar='PROJECT',
+ help='search packages built for PROJECT (implies --binary)')
+ @cmdln.option('--binaryversion', metavar='VERSION',
+ help='search for binary with specified version (implies --binary)')
+ @cmdln.alias('se')
+ @cmdln.alias('bse')
+ def do_search(self, subcmd, opts, *args):
+ """${cmd_name}: Search for a project and/or package.
+
+ If no option is specified osc will search for projects and
+ packages which contains the \'search term\' in their name,
+ title or description.
+
+ usage:
+ osc search \'search term\' <options>
+ osc bse ... ('osc search --binary')
+ osc se 'perl(Foo::Bar)' ('osc --package perl-Foo-Bar')
+ ${cmd_option_list}
+ """
+ def build_xpath(attr, what, substr = False):
+ if substr:
+ return 'contains(%s, \'%s\')' % (attr, what)
+ else:
+ return '%s = \'%s\'' % (attr, what)
+
+ search_term = ''
+ if len(args) > 1:
+ raise oscerr.WrongArgs('Too many arguments')
+ elif len(args) == 0:
+ if opts.involved or opts.bugowner or opts.maintainer or opts.mine:
+ search_term = conf.get_apiurl_usr(conf.config['apiurl'])
+ else:
+ raise oscerr.WrongArgs('Too few arguments')
+ else:
+ search_term = args[0]
+
+ if opts.maintained:
+ raise oscerr.WrongOptions('The --maintained option is not anymore supported. Please use the maintained command instead.')
+
+ # XXX: is it a good idea to make this the default?
+ # support perl symbols:
+ if re.match('^perl\(\w+(::\w+)*\)$', search_term):
+ search_term = re.sub('\)', '', re.sub('(::|\()', '-', search_term))
+ opts.package = True
+
+ if opts.mine:
+ opts.bugowner = True
+ opts.package = True
+
+ if (opts.title or opts.description) and (opts.involved or opts.bugowner or opts.maintainer):
+ raise oscerr.WrongOptions('Sorry, the options \'--title\' and/or \'--description\' ' \
+ 'are mutually exclusive with \'-i\'/\'-b\'/\'-m\'/\'-M\'')
+ if opts.substring and opts.exact:
+ raise oscerr.WrongOptions('Sorry, the options \'--substring\' and \'--exact\' are mutually exclusive')
+
+ if not opts.substring:
+ opts.exact = True
+ if subcmd == 'bse' or opts.baseproject or opts.binaryversion:
+ opts.binary = True
+
+ if opts.binary and (opts.title or opts.description or opts.involved or opts.bugowner or opts.maintainer
+ or opts.project or opts.package):
+ raise oscerr.WrongOptions('Sorry, \'--binary\' and \'--title\' or \'--description\' or \'--involved ' \
+ 'or \'--bugowner\' or \'--maintainer\' or \'--limit-to-attribute <attr>\ ' \
+ 'or \'--project\' or \'--package\' are mutually exclusive')
+
+ apiurl = self.get_api_url()
+
+ xpath = ''
+ if opts.title:
+ xpath = xpath_join(xpath, build_xpath('title', search_term, opts.substring), inner=True)
+ if opts.description:
+ xpath = xpath_join(xpath, build_xpath('description', search_term, opts.substring), inner=True)
+ if opts.project or opts.package or opts.binary:
+ xpath = xpath_join(xpath, build_xpath('@name', search_term, opts.substring), inner=True)
+ # role filter
+ role_filter = ''
+ if opts.bugowner or opts.maintainer or opts.involved:
+ tmp = search_term.split(':')
+ if len(tmp) > 1:
+ search_type, search_term = [tmp[0], tmp[1]]
+ else:
+ search_type = 'person'
+ search_dict = { 'person' : 'userid',
+ 'group' : 'groupid' }
+ try:
+ search_id = search_dict[ search_type ]
+ except KeyError:
+ search_type, search_id = [ 'person', 'userid' ]
+ xpath = xpath_join(xpath, '%s/@%s = \'%s\'' % (search_type, search_id, search_term), inner=True)
+ role_filter = '%s (%s)' % (search_term, search_type)
+ role_filter_xpath = xpath
+ if opts.bugowner and not opts.maintainer:
+ xpath = xpath_join(xpath, '%s/@role=\'bugowner\'' % search_type, op='and')
+ role_filter = 'bugowner'
+ elif not opts.bugowner and opts.maintainer:
+ xpath = xpath_join(xpath, '%s/@role=\'maintainer\'' % search_type, op='and')
+ role_filter = 'maintainer'
+ if opts.limit_to_attribute:
+ xpath = xpath_join(xpath, 'attribute/@name=\'%s\'' % opts.limit_to_attribute, op='and')
+ if opts.baseproject:
+ xpath = xpath_join(xpath, 'path/@project=\'%s\'' % opts.baseproject, op='and')
+ if opts.binaryversion:
+ m = re.match(r'(.+)-(.*?)$', opts.binaryversion)
+ if m:
+ if m.group(2) != '':
+ xpath = xpath_join(xpath, '@versrel=\'%s\'' % opts.binaryversion, op='and')
+ else:
+ xpath = xpath_join(xpath, '@version=\'%s\'' % m.group(1), op='and')
+ else:
+ xpath = xpath_join(xpath, '@version=\'%s\'' % opts.binaryversion, op='and')
+
+ if not xpath:
+ xpath = xpath_join(xpath, build_xpath('@name', search_term, opts.substring), inner=True)
+ xpath = xpath_join(xpath, build_xpath('title', search_term, opts.substring), inner=True)
+ xpath = xpath_join(xpath, build_xpath('description', search_term, opts.substring), inner=True)
+ what = {'project': xpath, 'package': xpath}
+ if opts.project and not opts.package:
+ what = {'project': xpath}
+ elif not opts.project and opts.package:
+ what = {'package': xpath}
+ elif opts.binary:
+ what = {'published/binary/id': xpath}
+ try:
+ res = search(apiurl, **what)
+ except HTTPError as e:
+ if e.code != 400 or not role_filter:
+ raise e
+ # backward compatibility: local role filtering
+ if opts.limit_to_attribute:
+ role_filter_xpath = xpath_join(role_filter_xpath, 'attribute/@name=\'%s\'' % opts.limit_to_attribute, op='and')
+ what = dict([[kind, role_filter_xpath] for kind in what.keys()])
+ res = search(apiurl, **what)
+ filter_role(res, search_term, role_filter)
+ if role_filter:
+ role_filter = '%s (%s)' % (search_term, role_filter)
+ kind_map = {'published/binary/id': 'binary'}
+ for kind, root in res.items():
+ results = []
+ for node in root.findall(kind_map.get(kind, kind)):
+ result = []
+ project = node.get('project')
+ package = None
+ if project is None:
+ project = node.get('name')
+ else:
+ if kind == 'published/binary/id':
+ package = node.get('package')
+ else:
+ package = node.get('name')
+
+ result.append(project)
+ if not package is None:
+ result.append(package)
+
+ if opts.version and package != None:
+ sr = get_source_rev(apiurl, project, package)
+ v = sr.get('version')
+ r = sr.get('rev')
+ s = sr.get('srcmd5')
+ if not v or v == 'unknown':
+ v = '-'
+ if not r:
+ r = '-'
+ if not s:
+ s = '-'
+ result.append(v)
+ result.append(r)
+ result.append(s)
+
+ if opts.verbose:
+ title = node.findtext('title').strip()
+ if len(title) > 60:
+ title = title[:61] + '...'
+ result.append(title)
+
+ if opts.repos_baseurl:
+ # FIXME: no hardcoded URL of instance
+ result.append('http://download.opensuse.org/repositories/%s/' % project.replace(':', ':/'))
+ if kind == 'published/binary/id':
+ result.append(node.get('filepath'))
+ results.append(result)
+
+ if not len(results):
+ print('No matches found for \'%s\' in %ss' % (role_filter or search_term, kind))
+ continue
+ # construct a sorted, flat list
+ # Sort by first column, follwed by second column if we have two columns, else sort by first.
+ results.sort(lambda x, y: ( cmp(x[0], y[0]) or
+ (len(x)>1 and len(y)>1 and cmp(x[1], y[1])) ))
+ new = []
+ for i in results:
+ new.extend(i)
+ results = new
+ headline = []
+ if kind == 'package' or kind == 'published/binary/id':
+ headline = [ '# Project', '# Package' ]
+ else:
+ headline = [ '# Project' ]
+ if opts.version and kind == 'package':
+ headline.append('# Ver')
+ headline.append('Rev')
+ headline.append('Srcmd5')
+ if opts.verbose:
+ headline.append('# Title')
+ if opts.repos_baseurl:
+ headline.append('# URL')
+ if opts.binary:
+ headline.append('# filepath')
+ if not opts.csv:
+ if len(what.keys()) > 1:
+ print('#' * 68)
+ print('matches for \'%s\' in %ss:\n' % (role_filter or search_term, kind))
+ for row in build_table(len(headline), results, headline, 2, csv = opts.csv):
+ print(row)
+
+
+ @cmdln.option('-p', '--project', metavar='project',
+ help='specify the path to a project')
+ @cmdln.option('-n', '--name', metavar='name',
+ help='specify a package name')
+ @cmdln.option('-t', '--title', metavar='title',
+ help='set a title')
+ @cmdln.option('-d', '--description', metavar='description',
+ help='set the description of the package')
+ @cmdln.option('', '--delete-old-files', action='store_true',
+ help='delete existing files from the server')
+ @cmdln.option('-c', '--commit', action='store_true',
+ help='commit the new files')
+ def do_importsrcpkg(self, subcmd, opts, srpm):
+ """${cmd_name}: Import a new package from a src.rpm
+
+ A new package dir will be created inside the project dir
+ (if no project is specified and the current working dir is a
+ project dir the package will be created in this project). If
+ the package does not exist on the server it will be created
+ too otherwise the meta data of the existing package will be
+ updated (<title /> and <description />).
+ The src.rpm will be extracted into the package dir. The files
+ won't be committed unless you explicitly pass the --commit switch.
+
+ SRPM is the path of the src.rpm in the local filesystem,
+ or an URL.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ import glob
+ from .util import rpmquery
+
+ if opts.delete_old_files and conf.config['do_package_tracking']:
+ # IMHO the --delete-old-files option doesn't really fit into our
+ # package tracking strategy
+ print('--delete-old-files is not supported anymore', file=sys.stderr)
+ print('when do_package_tracking is enabled', file=sys.stderr)
+ sys.exit(1)
+
+ if '://' in srpm:
+ print('trying to fetch', srpm)
+ import urlgrabber
+ urlgrabber.urlgrab(srpm)
+ srpm = os.path.basename(srpm)
+
+ srpm = os.path.abspath(srpm)
+ if not os.path.isfile(srpm):
+ print('file \'%s\' does not exist' % srpm, file=sys.stderr)
+ sys.exit(1)
+
+ if opts.project:
+ project_dir = opts.project
+ else:
+ project_dir = os.curdir
+
+ if conf.config['do_package_tracking']:
+ project = Project(project_dir)
+ else:
+ project = store_read_project(project_dir)
+
+ rpmq = rpmquery.RpmQuery.query(srpm)
+ title, pac, descr, url = rpmq.summary(), rpmq.name(), rpmq.description(), rpmq.url()
+ if url is None:
+ url = ''
+
+ if opts.title:
+ title = opts.title
+ if opts.name:
+ pac = opts.name
+ if opts.description:
+ descr = opts.description
+
+ # title and description can be empty
+ if not pac:
+ print('please specify a package name with the \'--name\' option. ' \
+ 'The automatic detection failed', file=sys.stderr)
+ sys.exit(1)
+
+ if conf.config['do_package_tracking']:
+ createPackageDir(os.path.join(project.dir, pac), project)
+ else:
+ if not os.path.exists(os.path.join(project_dir, pac)):
+ apiurl = store_read_apiurl(project_dir)
+ user = conf.get_apiurl_usr(apiurl)
+ data = meta_exists(metatype='pkg',
+ path_args=(quote_plus(project), quote_plus(pac)),
+ template_args=({
+ 'name': pac,
+ 'user': user}), apiurl=apiurl)
+ if data:
+ data = ET.fromstring(''.join(data))
+ data.find('title').text = ''.join(title)
+ data.find('description').text = ''.join(descr)
+ data.find('url').text = url
+ data = ET.tostring(data, encoding=ET_ENCODING)
+ else:
+ print('error - cannot get meta data', file=sys.stderr)
+ sys.exit(1)
+ edit_meta(metatype='pkg',
+ path_args=(quote_plus(project), quote_plus(pac)),
+ data = data, apiurl=apiurl)
+ Package.init_package(apiurl, project, pac, os.path.join(project_dir, pac))
+ else:
+ print('error - local package already exists', file=sys.stderr)
+ sys.exit(1)
+
+ unpack_srcrpm(srpm, os.path.join(project_dir, pac))
+ p = Package(os.path.join(project_dir, pac))
+ if len(p.filenamelist) == 0 and opts.commit:
+ print('Adding files to working copy...')
+ addFiles(glob.glob('%s/*' % os.path.join(project_dir, pac)))
+ if conf.config['do_package_tracking']:
+ project.commit((pac, ))
+ else:
+ p.update_datastructs()
+ p.commit()
+ elif opts.commit and opts.delete_old_files:
+ for filename in p.filenamelist:
+ p.delete_remote_source_file(filename)
+ p.update_local_filesmeta()
+ print('Adding files to working copy...')
+ addFiles(glob.glob('*'))
+ p.update_datastructs()
+ p.commit()
+ else:
+ print('No files were committed to the server. Please ' \
+ 'commit them manually.')
+ print('Package \'%s\' only imported locally' % pac)
+ sys.exit(1)
+
+ print('Package \'%s\' imported successfully' % pac)
+
+
+ @cmdln.option('-X', '-m', '--method', default='GET', metavar='HTTP_METHOD',
+ help='specify HTTP method to use (GET|PUT|DELETE|POST)')
+ @cmdln.option('-e', '--edit', default=None, action='store_true',
+ help='GET, edit and PUT the location')
+ @cmdln.option('-d', '--data', default=None, metavar='STRING',
+ help='specify string data for e.g. POST')
+ @cmdln.option('-T', '-f', '--file', default=None, metavar='FILE',
+ help='specify filename to upload, uses PUT mode by default')
+ @cmdln.option('-a', '--add-header', default=None, metavar='NAME STRING',
+ nargs=2, action='append', dest='headers',
+ help='add the specified header to the request')
+ def do_api(self, subcmd, opts, url):
+ """${cmd_name}: Issue an arbitrary request to the API
+
+ Useful for testing.
+
+ URL can be specified either partially (only the path component), or fully
+ with URL scheme and hostname ('http://...').
+
+ Note the global -A and -H options (see osc help).
+
+ Examples:
+ osc api /source/home:user
+ osc api -X PUT -T /etc/fstab source/home:user/test5/myfstab
+ osc api -e /configuration
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+
+ if not opts.method in ['GET', 'PUT', 'POST', 'DELETE']:
+ sys.exit('unknown method %s' % opts.method)
+
+ # default is PUT when uploading files
+ if opts.file and opts.method == 'GET':
+ opts.method = 'PUT'
+
+ if not url.startswith('http'):
+ if not url.startswith('/'):
+ url = '/' + url
+ url = apiurl + url
+
+ if opts.headers:
+ opts.headers = dict(opts.headers)
+
+ r = http_request(opts.method,
+ url,
+ data=opts.data,
+ file=opts.file,
+ headers=opts.headers)
+ out = r.read()
+
+ if opts.edit:
+ text = edit_text(out)
+ r = http_request("PUT",
+ url,
+ data=text,
+ headers=opts.headers)
+ out = r.read()
+
+ sys.stdout.write(out)
+
+
+ @cmdln.option('-b', '--bugowner-only', action='store_true',
+ help='Show only the bugowner')
+ @cmdln.option('-B', '--bugowner', action='store_true',
+ help='Show only the bugowner if defined, or maintainer otherwise')
+ @cmdln.option('-e', '--email', action='store_true',
+ help='show email addresses instead of user names')
+ @cmdln.option('--nodevelproject', action='store_true',
+ help='do not follow a defined devel project ' \
+ '(primary project where a package is developed)')
+ @cmdln.option('-v', '--verbose', action='store_true',
+ help='show more information')
+ @cmdln.option('-D', '--devel-project', metavar='devel_project',
+ help='define the project where this package is primarily developed')
+ @cmdln.option('-a', '--add', metavar='user',
+ help='add a new person for given role ("maintainer" by default)')
+ @cmdln.option('-A', '--all', action='store_true',
+ help='list all found entries not just the first one')
+ @cmdln.option('-s', '--set-bugowner', metavar='user',
+ help='Set the bugowner to specified person (or group via group: prefix)')
+ @cmdln.option('-S', '--set-bugowner-request', metavar='user',
+ help='Set the bugowner to specified person via a request (or group via group: prefix)')
+ @cmdln.option('-U', '--user', metavar='USER',
+ help='All official maintained instances for the specified USER')
+ @cmdln.option('-G', '--group', metavar='GROUP',
+ help='All official maintained instances for the specified GROUP')
+ @cmdln.option('-d', '--delete', metavar='user',
+ help='delete a maintainer/bugowner (can be specified via --role)')
+ @cmdln.option('-r', '--role', metavar='role', action='append', default=[],
+ help='Specify user role')
+ @cmdln.option('-m', '--message',
+ help='Define message as commit entry or request description')
+ @cmdln.alias('bugowner')
+ def do_maintainer(self, subcmd, opts, *args):
+ """${cmd_name}: Show maintainers according to server side configuration
+
+ # Search for official maintained sources in OBS instance
+ osc maintainer BINARY <options>
+ osc maintainer -U <user> <options>
+ osc maintainer -G <group> <options>
+
+ # Lookup via containers
+ osc maintainer <options>
+ osc maintainer PRJ <options>
+ osc maintainer PRJ PKG <options>
+
+ The tool looks up the default responsible person for a certain project or package.
+ When using with an OBS 2.4 (or later) server it is doing the lookup for
+ a given binary according to the server side configuration of default owners.
+
+ The tool is also looking into devel packages and supports to fallback to the project
+ in case a package has no defined maintainer.
+
+ Please use "osc meta pkg" in case you need to know the definition in a specific container.
+
+ PRJ and PKG default to current working-copy path.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ def get_maintainer_data(apiurl, maintainer, verbose=False):
+ tags = ('email',)
+ if maintainer.startswith('group:'):
+ group = maintainer.replace('group:', '')
+ if verbose:
+ return [maintainer] + get_group_data(apiurl, group, 'title', *tags)
+ return get_group_data(apiurl, group, 'email')
+ if verbose:
+ tags = ('login', 'realname', 'email')
+ return get_user_data(apiurl, maintainer, *tags)
+ def setBugownerHelper(apiurl, project, package, bugowner):
+ try:
+ setBugowner(apiurl, project, package, bugowner)
+ except HTTPError as e:
+ if e.code != 403:
+ raise
+ print("No write permission in", project, end=' ')
+ if package:
+ print("/", package, end=' ')
+ print()
+ repl = raw_input('\nCreating a request instead? (y/n) ')
+ if repl.lower() == 'y':
+ opts.set_bugowner_request = bugowner
+ opts.set_bugowner = None
+
+ binary = None
+ prj = None
+ pac = None
+ metaroot = None
+ searchresult = None
+ roles = [ 'bugowner', 'maintainer' ]
+ if len(opts.role):
+ roles = opts.role
+ elif opts.bugowner_only or opts.bugowner or subcmd == 'bugowner':
+ roles = [ 'bugowner' ]
+
+ args = slash_split(args)
+ if opts.user or opts.group:
+ if len(args) != 0:
+ raise oscerr.WrongArgs('Either search for user or for packages.')
+ elif len(args) == 0:
+ try:
+ pac = store_read_package('.')
+ except oscerr.NoWorkingCopy:
+ pass
+ prj = store_read_project('.')
+ elif len(args) == 1:
+ # it is unclear if one argument is a binary or a project, try binary first for new OBS 2.4
+ binary = prj = args[0]
+ elif len(args) == 2:
+ prj = args[0]
+ pac = args[1]
+ else:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+
+ apiurl = self.get_api_url()
+
+ # Try the OBS 2.4 way first.
+ if binary or opts.user or opts.group:
+ limit = None
+ if opts.all:
+ limit = 0
+ filterroles = roles
+ if filterroles == [ 'bugowner', 'maintainer' ]:
+ # use server side configured default
+ filterroles = None
+ if binary:
+ searchresult = owner(apiurl, binary, "binary", usefilter=filterroles, devel=None, limit=limit)
+ if searchresult != None and len(searchresult) == 0:
+ # We talk to an OBS 2.4 or later understanding the call
+ if opts.set_bugowner or opts.set_bugowner_request:
+ # filtered search did not succeed, but maybe we want to set an owner initially?
+ searchresult = owner(apiurl, binary, "binary", usefilter="", devel=None, limit=-1)
+ if searchresult:
+ print("WARNING: the binary exists, but has no matching maintainership roles defined.")
+ print("Do you want to set it in the container where the binary appeared first?")
+ result = searchresult.find('owner')
+ print("This is: " + result.get('project'), end=' ')
+ if result.get('package'):
+ print (" / " + result.get('package'))
+ repl = raw_input('\nUse this container? (y/n) ')
+ if repl.lower() != 'y':
+ searchresult = None
+ else:
+ print("Empty search result, you may want to search with other or all roles via -r ''")
+ return
+ elif opts.user:
+ searchresult = owner(apiurl, opts.user, "user", usefilter=filterroles, devel=None)
+ elif opts.group:
+ searchresult = owner(apiurl, opts.group, "group", usefilter=filterroles, devel=None)
+ else:
+ raise oscerr.WrongArgs('osc bug, no valid search criteria')
+
+ if opts.add:
+ if searchresult:
+ for result in searchresult.findall('owner'):
+ for role in roles:
+ addPerson(apiurl, result.get('project'), result.get('package'), opts.add, role)
+ else:
+ for role in roles:
+ addPerson(apiurl, prj, pac, opts.add, role)
+ elif opts.set_bugowner or opts.set_bugowner_request:
+ bugowner = opts.set_bugowner or opts.set_bugowner_request
+ requestactionsxml = ""
+ if searchresult:
+ for result in searchresult.findall('owner'):
+ if opts.set_bugowner:
+ setBugownerHelper(apiurl, result.get('project'), result.get('package'), opts.set_bugowner)
+ if opts.set_bugowner_request:
+ args = [bugowner, result.get('project')]
+ if result.get('package'):
+ args = args + [result.get('package')]
+ requestactionsxml += self._set_bugowner(args, opts)
+
+ else:
+ if opts.set_bugowner:
+ setBugownerHelper(apiurl, prj, pac, opts.set_bugowner)
+
+ if opts.set_bugowner_request:
+ args = [bugowner, prj]
+ if pac:
+ args = args + [pac]
+ requestactionsxml += self._set_bugowner(args, opts)
+
+ if requestactionsxml != "":
+ if opts.message:
+ message = opts.message
+ else:
+ message = edit_message()
+
+ import cgi
+ xml = """<request> %s <state name="new"/> <description>%s</description> </request> """ % \
+ (requestactionsxml, cgi.escape(message or ""))
+ u = makeurl(apiurl, ['request'], query='cmd=create')
+ f = http_POST(u, data=xml)
+
+ root = ET.parse(f).getroot()
+ print("Request ID:", root.get('id'))
+
+ elif opts.delete:
+ if searchresult:
+ for result in searchresult.findall('owner'):
+ for role in roles:
+ delPerson(apiurl, result.get('project'), result.get('package'), opts.delete, role)
+ else:
+ for role in roles:
+ delPerson(apiurl, prj, pac, opts.delete, role)
+ elif opts.devel_project:
+ # XXX: does it really belong to this command?
+ setDevelProject(apiurl, prj, pac, opts.devel_project)
+ else:
+ if pac:
+ m = show_package_meta(apiurl, prj, pac)
+ metaroot = ET.fromstring(''.join(m))
+ if not opts.nodevelproject:
+ while metaroot.findall('devel'):
+ d = metaroot.find('devel')
+ prj = d.get('project', prj)
+ pac = d.get('package', pac)
+ if opts.verbose:
+ print("Following to the development space: %s/%s" % (prj, pac))
+ m = show_package_meta(apiurl, prj, pac)
+ metaroot = ET.fromstring(''.join(m))
+ if not metaroot.findall('person') and not metaroot.findall('group'):
+ if opts.verbose:
+ print("No dedicated persons in package defined, showing the project persons.")
+ pac = None
+ m = show_project_meta(apiurl, prj)
+ metaroot = ET.fromstring(''.join(m))
+ else:
+ # fallback to project lookup for old servers
+ if prj and not searchresult:
+ m = show_project_meta(apiurl, prj)
+ metaroot = ET.fromstring(''.join(m))
+
+ # extract the maintainers
+ projects = []
+ # from owner search
+ if searchresult:
+ for result in searchresult.findall('owner'):
+ maintainers = {}
+ maintainers.setdefault("project", result.get('project'))
+ maintainers.setdefault("package", result.get('package'))
+ for person in result.findall('person'):
+ maintainers.setdefault(person.get('role'), []).append(person.get('name'))
+ for group in result.findall('group'):
+ maintainers.setdefault(group.get('role'), []).append("group:"+group.get('name'))
+ projects = projects + [maintainers]
+ # from meta data
+ if metaroot:
+ # we have just one result
+ maintainers = {}
+ for person in metaroot.findall('person'):
+ maintainers.setdefault(person.get('role'), []).append(person.get('userid'))
+ for group in metaroot.findall('group'):
+ maintainers.setdefault(group.get('role'), []).append("group:"+group.get('groupid'))
+ projects = [maintainers]
+
+ # showing the maintainers
+ for maintainers in projects:
+ indent = ""
+ definingproject = maintainers.get("project")
+ if definingproject:
+ definingpackage = maintainers.get("package")
+ indent = " "
+ if definingpackage:
+ print("Defined in package: %s/%s " % (definingproject, definingpackage))
+ else:
+ print("Defined in project: ", definingproject)
+
+ if prj:
+ # not for user/group search
+ for role in roles:
+ if opts.bugowner and not len(maintainers.get(role, [])):
+ role = 'maintainer'
+ if pac:
+ print("%s%s of %s/%s : " %(indent, role, prj, pac))
+ else:
+ print("%s%s of %s : " %(indent, role, prj))
+ if opts.email:
+ emails = []
+ for maintainer in maintainers.get(role, []):
+ user = get_maintainer_data(apiurl, maintainer, verbose=False)
+ if len(user):
+ emails.append(''.join(user))
+ print(indent, end=' ')
+ print(', '.join(emails) or '-')
+ elif opts.verbose:
+ userdata = []
+ for maintainer in maintainers.get(role, []):
+ user = get_maintainer_data(apiurl, maintainer, verbose=True)
+ userdata.append(user[0])
+ if user[1] != '-':
+ userdata.append("%s <%s>"%(user[1], user[2]))
+ else:
+ userdata.append(user[2])
+ for row in build_table(2, userdata, None, 3):
+ print(indent, end=' ')
+ print(row)
+ else:
+ print(indent, end=' ')
+ print(', '.join(maintainers.get(role, [])) or '-')
+ print()
+
+ @cmdln.alias('who')
+ @cmdln.alias('user')
+ def do_whois(self, subcmd, opts, *usernames):
+ """${cmd_name}: Show fullname and email of a buildservice user
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ apiurl = self.get_api_url()
+ if len(usernames) < 1:
+ if 'user' not in conf.config['api_host_options'][apiurl]:
+ raise oscerr.WrongArgs('your .oscrc does not have your user name.')
+ usernames = (conf.config['api_host_options'][apiurl]['user'],)
+ for name in usernames:
+ user = get_user_data(apiurl, name, 'login', 'realname', 'email')
+ if len(user) == 3:
+ print("%s: \"%s\" <%s>"%(user[0], user[1], user[2]))
+
+
+ @cmdln.option('-r', '--revision', metavar='rev',
+ help='print out the specified revision')
+ @cmdln.option('-e', '--expand', action='store_true',
+ help='force expansion of linked packages.')
+ @cmdln.option('-u', '--unexpand', action='store_true',
+ help='always work with unexpanded packages.')
+ @cmdln.option('-M', '--meta', action='store_true',
+ help='list meta data files')
+ @cmdln.alias('less')
+ def do_cat(self, subcmd, opts, *args):
+ """${cmd_name}: Output the content of a file to standard output
+
+ Examples:
+ osc cat project package file
+ osc cat project/package/file
+ osc cat http://api.opensuse.org/build/.../_log
+ osc cat http://api.opensuse.org/source/../_link
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ if len(args) == 1 and (args[0].startswith('http://') or
+ args[0].startswith('https://')):
+ opts.method = 'GET'
+ opts.headers = None
+ opts.data = None
+ opts.file = None
+ return self.do_api('list', opts, *args)
+
+
+
+ args = slash_split(args)
+ if len(args) != 3:
+ raise oscerr.WrongArgs('Wrong number of arguments.')
+ rev, dummy = parseRevisionOption(opts.revision)
+ apiurl = self.get_api_url()
+
+ query = { }
+ if opts.meta:
+ query['meta'] = 1
+ if opts.revision:
+ query['rev'] = opts.revision
+ if opts.expand:
+ query['rev'] = show_upstream_srcmd5(apiurl, args[0], args[1], expand=True, revision=opts.revision, meta=opts.meta)
+ u = makeurl(apiurl, ['source', args[0], args[1], args[2]], query=query)
+ try:
+ if subcmd == 'less':
+ f = http_GET(u)
+ run_pager(''.join(f.readlines()))
+ else:
+ for data in streamfile(u):
+ sys.stdout.write(data)
+ except HTTPError as e:
+ if e.code == 404 and not opts.expand and not opts.unexpand:
+ print('expanding link...', file=sys.stderr)
+ query['rev'] = show_upstream_srcmd5(apiurl, args[0], args[1], expand=True, revision=opts.revision)
+ u = makeurl(apiurl, ['source', args[0], args[1], args[2]], query=query)
+ if subcmd == "less":
+ f = http_GET(u)
+ run_pager(''.join(f.readlines()))
+ else:
+ for data in streamfile(u):
+ sys.stdout.write(data)
+ else:
+ e.osc_msg = 'If linked, try: cat -e'
+ raise e
+
+
+ # helper function to download a file from a specific revision
+ def download(self, name, md5, dir, destfile):
+ o = open(destfile, 'wb')
+ if md5 != '':
+ query = {'rev': dir['srcmd5']}
+ u = makeurl(dir['apiurl'], ['source', dir['project'], dir['package'], pathname2url(name)], query=query)
+ for buf in streamfile(u, http_GET, BUFSIZE):
+ o.write(buf)
+ o.close()
+
+
+ @cmdln.option('-d', '--destdir', default='repairlink', metavar='DIR',
+ help='destination directory')
+ def do_repairlink(self, subcmd, opts, *args):
+ """${cmd_name}: Repair a broken source link
+
+ This command checks out a package with merged source changes. It uses
+ a 3-way merge to resolve file conflicts. After reviewing/repairing
+ the merge, use 'osc resolved ...' and 'osc ci' to re-create a
+ working source link.
+
+ usage:
+ * For merging conflicting changes of a checkout package:
+ osc repairlink
+
+ * Check out a package and merge changes:
+ osc repairlink PROJECT PACKAGE
+
+ * Pull conflicting changes from one project into another one:
+ osc repairlink PROJECT PACKAGE INTO_PROJECT [INTO_PACKAGE]
+
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ args = slash_split(args)
+ if len(args) >= 3 and len(args) <= 4:
+ prj = args[0]
+ package = target_package = args[1]
+ target_prj = args[2]
+ if len(args) == 4:
+ target_package = args[3]
+ elif len(args) == 2:
+ target_prj = prj = args[0]
+ target_package = package = args[1]
+ elif is_package_dir(os.getcwd()):
+ target_prj = prj = store_read_project(os.getcwd())
+ target_package = package = store_read_package(os.getcwd())
+ else:
+ raise oscerr.WrongArgs('Please specify project and package')
+
+ # first try stored reference, then lastworking
+ query = { 'rev': 'latest' }
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ raise oscerr.APIError('package is not a source link')
+ if linkinfo.get('error') == None:
+ raise oscerr.APIError('source link is not broken')
+ workingrev = None
+
+ if linkinfo.get('baserev'):
+ query = { 'rev': 'latest', 'linkrev': 'base' }
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo.get('error') == None:
+ workingrev = linkinfo.get('xsrcmd5')
+
+ if workingrev == None:
+ query = { 'lastworking': 1 }
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ linkinfo = root.find('linkinfo')
+ if linkinfo == None:
+ raise oscerr.APIError('package is not a source link')
+ if linkinfo.get('error') == None:
+ raise oscerr.APIError('source link is not broken')
+ workingrev = linkinfo.get('lastworking')
+ if workingrev == None:
+ raise oscerr.APIError('source link never worked')
+ print("using last working link target")
+ else:
+ print("using link target of last commit")
+
+ query = { 'expand': 1, 'emptylink': 1 }
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ meta = f.readlines()
+ root_new = ET.fromstring(''.join(meta))
+ dir_new = { 'apiurl': apiurl, 'project': prj, 'package': package }
+ dir_new['srcmd5'] = root_new.get('srcmd5')
+ dir_new['entries'] = [[n.get('name'), n.get('md5')] for n in root_new.findall('entry')]
+
+ query = { 'rev': workingrev }
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ root_oldpatched = ET.parse(f).getroot()
+ linkinfo_oldpatched = root_oldpatched.find('linkinfo')
+ if linkinfo_oldpatched == None:
+ raise oscerr.APIError('working rev is not a source link?')
+ if linkinfo_oldpatched.get('error') != None:
+ raise oscerr.APIError('working rev is not working?')
+ dir_oldpatched = { 'apiurl': apiurl, 'project': prj, 'package': package }
+ dir_oldpatched['srcmd5'] = root_oldpatched.get('srcmd5')
+ dir_oldpatched['entries'] = [[n.get('name'), n.get('md5')] for n in root_oldpatched.findall('entry')]
+
+ query = {}
+ query['rev'] = linkinfo_oldpatched.get('srcmd5')
+ u = makeurl(apiurl, ['source', linkinfo_oldpatched.get('project'), linkinfo_oldpatched.get('package')], query=query)
+ f = http_GET(u)
+ root_old = ET.parse(f).getroot()
+ dir_old = { 'apiurl': apiurl }
+ dir_old['project'] = linkinfo_oldpatched.get('project')
+ dir_old['package'] = linkinfo_oldpatched.get('package')
+ dir_old['srcmd5'] = root_old.get('srcmd5')
+ dir_old['entries'] = [[n.get('name'), n.get('md5')] for n in root_old.findall('entry')]
+
+ entries_old = dict(dir_old['entries'])
+ entries_oldpatched = dict(dir_oldpatched['entries'])
+ entries_new = dict(dir_new['entries'])
+
+ entries = {}
+ entries.update(entries_old)
+ entries.update(entries_oldpatched)
+ entries.update(entries_new)
+
+ destdir = opts.destdir
+ if os.path.isdir(destdir):
+ shutil.rmtree(destdir)
+ os.mkdir(destdir)
+
+ Package.init_package(apiurl, target_prj, target_package, destdir)
+ store_write_string(destdir, '_files', ''.join(meta) + '\n')
+ store_write_string(destdir, '_linkrepair', '')
+ pac = Package(destdir)
+
+ storedir = os.path.join(destdir, store)
+
+ for name in sorted(entries.keys()):
+ md5_old = entries_old.get(name, '')
+ md5_new = entries_new.get(name, '')
+ md5_oldpatched = entries_oldpatched.get(name, '')
+ if md5_new != '':
+ self.download(name, md5_new, dir_new, os.path.join(storedir, name))
+ if md5_old == md5_new:
+ if md5_oldpatched == '':
+ pac.put_on_deletelist(name)
+ continue
+ print(statfrmt(' ', name))
+ self.download(name, md5_oldpatched, dir_oldpatched, os.path.join(destdir, name))
+ continue
+ if md5_old == md5_oldpatched:
+ if md5_new == '':
+ continue
+ print(statfrmt('U', name))
+ shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name))
+ continue
+ if md5_new == md5_oldpatched:
+ if md5_new == '':
+ continue
+ print(statfrmt('G', name))
+ shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name))
+ continue
+ self.download(name, md5_oldpatched, dir_oldpatched, os.path.join(destdir, name + '.mine'))
+ if md5_new != '':
+ shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name + '.new'))
+ else:
+ self.download(name, md5_new, dir_new, os.path.join(destdir, name + '.new'))
+ self.download(name, md5_old, dir_old, os.path.join(destdir, name + '.old'))
+
+ if binary_file(os.path.join(destdir, name + '.mine')) or \
+ binary_file(os.path.join(destdir, name + '.old')) or \
+ binary_file(os.path.join(destdir, name + '.new')):
+ shutil.copy2(os.path.join(destdir, name + '.new'), os.path.join(destdir, name))
+ print(statfrmt('C', name))
+ pac.put_on_conflictlist(name)
+ continue
+
+ o = open(os.path.join(destdir, name), 'wb')
+ code = run_external('diff3', '-m', '-E',
+ '-L', '.mine',
+ os.path.join(destdir, name + '.mine'),
+ '-L', '.old',
+ os.path.join(destdir, name + '.old'),
+ '-L', '.new',
+ os.path.join(destdir, name + '.new'),
+ stdout=o)
+ if code == 0:
+ print(statfrmt('G', name))
+ os.unlink(os.path.join(destdir, name + '.mine'))
+ os.unlink(os.path.join(destdir, name + '.old'))
+ os.unlink(os.path.join(destdir, name + '.new'))
+ elif code == 1:
+ print(statfrmt('C', name))
+ pac.put_on_conflictlist(name)
+ else:
+ print(statfrmt('?', name))
+ pac.put_on_conflictlist(name)
+
+ pac.write_deletelist()
+ pac.write_conflictlist()
+ print()
+ print('Please change into the \'%s\' directory,' % destdir)
+ print('fix the conflicts (files marked with \'C\' above),')
+ print('run \'osc resolved ...\', and commit the changes.')
+
+
+ def do_pull(self, subcmd, opts, *args):
+ """${cmd_name}: merge the changes of the link target into your working copy.
+
+ ${cmd_option_list}
+ """
+
+ if not is_package_dir('.'):
+ raise oscerr.NoWorkingCopy('Error: \'%s\' is not an osc working copy.' % os.path.abspath('.'))
+ p = Package('.')
+ # check if everything is committed
+ for filename in p.filenamelist:
+ state = p.status(filename)
+ if state != ' ' and state != 'S':
+ raise oscerr.WrongArgs('Please commit your local changes first!')
+ # check if we need to update
+ upstream_rev = p.latest_rev()
+ if not (p.isfrozen() or p.ispulled()):
+ raise oscerr.WrongArgs('osc pull makes only sense with a detached head, did you mean osc up?')
+ if p.rev != upstream_rev:
+ raise oscerr.WorkingCopyOutdated((p.absdir, p.rev, upstream_rev))
+ elif not p.islink():
+ raise oscerr.WrongArgs('osc pull only works on linked packages.')
+ elif not p.isexpanded():
+ raise oscerr.WrongArgs('osc pull only works on expanded links.')
+ linkinfo = p.linkinfo
+ baserev = linkinfo.baserev
+ if baserev == None:
+ raise oscerr.WrongArgs('osc pull only works on links containing a base revision.')
+
+ # get revisions we need
+ query = { 'expand': 1, 'emptylink': 1 }
+ u = makeurl(p.apiurl, ['source', p.prjname, p.name], query=query)
+ f = http_GET(u)
+ meta = f.readlines()
+ root_new = ET.fromstring(''.join(meta))
+ linkinfo_new = root_new.find('linkinfo')
+ if linkinfo_new == None:
+ raise oscerr.APIError('link is not a really a link?')
+ if linkinfo_new.get('error') != None:
+ raise oscerr.APIError('link target is broken')
+ if linkinfo_new.get('srcmd5') == baserev:
+ print("Already up-to-date.")
+ p.unmark_frozen()
+ return
+ dir_new = { 'apiurl': p.apiurl, 'project': p.prjname, 'package': p.name }
+ dir_new['srcmd5'] = root_new.get('srcmd5')
+ dir_new['entries'] = [[n.get('name'), n.get('md5')] for n in root_new.findall('entry')]
+
+ dir_oldpatched = { 'apiurl': p.apiurl, 'project': p.prjname, 'package': p.name, 'srcmd5': p.srcmd5 }
+ dir_oldpatched['entries'] = [[f.name, f.md5] for f in p.filelist]
+
+ query = { 'rev': linkinfo.srcmd5 }
+ u = makeurl(p.apiurl, ['source', linkinfo.project, linkinfo.package], query=query)
+ f = http_GET(u)
+ root_old = ET.parse(f).getroot()
+ dir_old = { 'apiurl': p.apiurl, 'project': linkinfo.project, 'package': linkinfo.package, 'srcmd5': linkinfo.srcmd5 }
+ dir_old['entries'] = [[n.get('name'), n.get('md5')] for n in root_old.findall('entry')]
+
+ # now do 3-way merge
+ entries_old = dict(dir_old['entries'])
+ entries_oldpatched = dict(dir_oldpatched['entries'])
+ entries_new = dict(dir_new['entries'])
+ entries = {}
+ entries.update(entries_old)
+ entries.update(entries_oldpatched)
+ entries.update(entries_new)
+ for name in sorted(entries.keys()):
+ if name.startswith('_service:') or name.startswith('_service_'):
+ continue
+ md5_old = entries_old.get(name, '')
+ md5_new = entries_new.get(name, '')
+ md5_oldpatched = entries_oldpatched.get(name, '')
+ if md5_old == md5_new or md5_oldpatched == md5_new:
+ continue
+ if md5_old == md5_oldpatched:
+ if md5_new == '':
+ print(statfrmt('D', name))
+ p.put_on_deletelist(name)
+ os.unlink(name)
+ elif md5_old == '':
+ print(statfrmt('A', name))
+ self.download(name, md5_new, dir_new, name)
+ p.put_on_addlist(name)
+ else:
+ print(statfrmt('U', name))
+ self.download(name, md5_new, dir_new, name)
+ continue
+ # need diff3 to resolve issue
+ if md5_oldpatched == '':
+ open(name, 'w').write('')
+ os.rename(name, name + '.mine')
+ self.download(name, md5_new, dir_new, name + '.new')
+ self.download(name, md5_old, dir_old, name + '.old')
+ if binary_file(name + '.mine') or binary_file(name + '.old') or binary_file(name + '.new'):
+ shutil.copy2(name + '.new', name)
+ print(statfrmt('C', name))
+ p.put_on_conflictlist(name)
+ continue
+
+ o = open(name, 'wb')
+ code = run_external('diff3', '-m', '-E',
+ '-L', '.mine', name + '.mine',
+ '-L', '.old', name + '.old',
+ '-L', '.new', name + '.new',
+ stdout=o)
+ if code == 0:
+ print(statfrmt('G', name))
+ os.unlink(name + '.mine')
+ os.unlink(name + '.old')
+ os.unlink(name + '.new')
+ elif code == 1:
+ print(statfrmt('C', name))
+ p.put_on_conflictlist(name)
+ else:
+ print(statfrmt('?', name))
+ p.put_on_conflictlist(name)
+ p.write_deletelist()
+ p.write_addlist()
+ p.write_conflictlist()
+ # store new linkrev
+ store_write_string(p.absdir, '_pulled', linkinfo_new.get('srcmd5') + '\n')
+ p.unmark_frozen()
+ print()
+ if len(p.in_conflict):
+ print('Please fix the conflicts (files marked with \'C\' above),')
+ print('run \'osc resolved ...\', and commit the changes')
+ print('to update the link information.')
+ else:
+ print('Please commit the changes to update the link information.')
+
+ @cmdln.option('--create', action='store_true', default=False,
+ help='create new gpg signing key for this project')
+ @cmdln.option('--extend', action='store_true', default=False,
+ help='extend expiration date of the gpg public key for this project')
+ @cmdln.option('--delete', action='store_true', default=False,
+ help='delete the gpg signing key in this project')
+ @cmdln.option('--notraverse', action='store_true', default=False,
+ help='don\' traverse projects upwards to find key')
+ @cmdln.option('--sslcert', action='store_true', default=False,
+ help='fetch SSL certificate instead of GPG key')
+ def do_signkey(self, subcmd, opts, *args):
+ """${cmd_name}: Manage Project Signing Key
+
+ osc signkey [--create|--delete|--extend] <PROJECT>
+ osc signkey [--notraverse] <PROJECT>
+
+ This command is for managing gpg keys. It shows the public key
+ by default. There is no way to download or upload the private
+ part of a key by design.
+
+ However you can create a new own key. You may want to consider
+ to sign the public key with your own existing key.
+
+ If a project has no key, the key from upper level project will
+ be used (eg. when dropping "KDE:KDE4:Community" key, the one from
+ "KDE:KDE4" will be used).
+
+ WARNING: THE OLD KEY WILL NOT BE RESTORABLE WHEN USING DELETE OR CREATE
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ apiurl = self.get_api_url()
+ f = None
+
+ prj = None
+ if len(args) == 0:
+ cwd = os.getcwd()
+ if is_project_dir(cwd) or is_package_dir(cwd):
+ prj = store_read_project(cwd)
+ if len(args) == 1:
+ prj = args[0]
+
+ if not prj:
+ raise oscerr.WrongArgs('Please specify just the project')
+
+ if opts.create:
+ url = makeurl(apiurl, ['source', prj], query='cmd=createkey')
+ f = http_POST(url)
+ elif opts.extend:
+ url = makeurl(apiurl, ['source', prj], query='cmd=extendkey')
+ f = http_POST(url)
+ elif opts.delete:
+ url = makeurl(apiurl, ['source', prj, "_pubkey"])
+ f = http_DELETE(url)
+ else:
+ while True:
+ try:
+ url = makeurl(apiurl, ['source', prj, '_pubkey'])
+ if opts.sslcert:
+ url = makeurl(apiurl, ['source', prj, '_project', '_sslcert'], 'meta=1')
+ f = http_GET(url)
+ break
+ except HTTPError as e:
+ l = prj.rsplit(':', 1)
+ # try key from parent project
+ if not opts.notraverse and len(l) > 1 and l[0] and l[1] and e.code == 404:
+ print('%s has no key, trying %s' % (prj, l[0]))
+ prj = l[0]
+ else:
+ raise
+
+ while True:
+ buf = f.read(16384)
+ if not buf:
+ break
+ sys.stdout.write(buf)
+
+ @cmdln.option('-m', '--message',
+ help='add MESSAGE to changes (do not open an editor)')
+ @cmdln.option('-F', '--file', metavar='FILE',
+ help='read changes message from FILE (do not open an editor)')
+ @cmdln.option('-e', '--just-edit', action='store_true', default=False,
+ help='just open changes (cannot be used with -m)')
+ def do_vc(self, subcmd, opts, *args):
+ """${cmd_name}: Edit the changes file
+
+ osc vc [-m MESSAGE|-e] [filename[.changes]|path [file_with_comment]]
+ If no <filename> is given, exactly one *.changes or *.spec file has to
+ be in the cwd or in path.
+
+ The email address used in .changes file is read from BuildService
+ instance, or should be defined in ~/.oscrc
+ [https://api.opensuse.org/]
+ user = login
+ pass = password
+ email = user@defined.email
+
+ or can be specified via mailaddr environment variable.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ from subprocess import Popen
+ if opts.message and opts.file:
+ raise oscerr.WrongOptions('\'--message\' and \'--file\' are mutually exclusive')
+ elif opts.message and opts.just_edit:
+ raise oscerr.WrongOptions('\'--message\' and \'--just-edit\' are mutually exclusive')
+ elif opts.file and opts.just_edit:
+ raise oscerr.WrongOptions('\'--file\' and \'--just-edit\' are mutually exclusive')
+ meego_style = False
+ if not args:
+ import glob, re
+ try:
+ fn_changelog = glob.glob('*.changes')[0]
+ fp = file(fn_changelog)
+ titleline = fp.readline()
+ fp.close()
+ if re.match('^\*\W+(.+\W+\d{1,2}\W+20\d{2})\W+(.+)\W+<(.+)>\W+(.+)$', titleline):
+ meego_style = True
+ except IndexError:
+ pass
+
+ cmd_list = [conf.config['vc-cmd']]
+ if meego_style:
+ if not os.path.exists('/usr/bin/vc'):
+ print('Error: you need meego-packaging-tools for /usr/bin/vc command', file=sys.stderr)
+ return 1
+ cmd_list = ['/usr/bin/vc']
+ elif which(cmd_list[0]) is None:
+ print('Error: vc (\'%s\') command not found' % cmd_list[0], file=sys.stderr)
+ print('Install the build package from http://download.opensuse.org/repositories/openSUSE:/Tools/', file=sys.stderr)
+ return 1
+
+ # set user's email if no mailaddr exists
+ if 'mailaddr' not in os.environ:
+
+ if len(args) and is_package_dir(args[0]):
+ apiurl = store_read_apiurl(args[0])
+ else:
+ apiurl = self.get_api_url()
+
+ user = conf.get_apiurl_usr(apiurl)
+
+ data = get_user_data(apiurl, user, 'email')
+ if data:
+ os.environ['mailaddr'] = data[0]
+ else:
+ print('Try env mailaddr=...', file=sys.stderr)
+
+ # mailaddr can be overrided by config one
+ if 'email' in conf.config['api_host_options'][apiurl]:
+ os.environ['mailaddr'] = conf.config['api_host_options'][apiurl]['email']
+
+ if meego_style:
+ if opts.message or opts.just_edit:
+ print('Warning: to edit MeeGo style changelog, opts will be ignored.', file=sys.stderr)
+ else:
+ if opts.message:
+ cmd_list.append("-m")
+ cmd_list.append(opts.message)
+ if opts.file:
+ if not os.path.isfile(opts.file):
+ raise oscerr.WrongOptions('\'%s\': is no file' % opts.file)
+ cmd_list.append("-m")
+ cmd_list.append(open(opts.file).read().strip())
+
+ if opts.just_edit:
+ cmd_list.append("-e")
+
+ cmd_list.extend(args)
+
+ vc = Popen(cmd_list)
+ vc.wait()
+ sys.exit(vc.returncode)
+
+ @cmdln.option('-f', '--force', action='store_true',
+ help='forces removal of entire package and its files')
+ def do_mv(self, subcmd, opts, source, dest):
+ """${cmd_name}: Move SOURCE file to DEST and keep it under version control
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+
+ if not os.path.isfile(source):
+ raise oscerr.WrongArgs("Source file '%s' does not exist or is not a file" % source)
+ if not opts.force and os.path.isfile(dest):
+ raise oscerr.WrongArgs("Dest file '%s' already exists" % dest)
+ if os.path.isdir(dest):
+ dest = os.path.join(dest, os.path.basename(source))
+ src_pkg = findpacs([source])
+ tgt_pkg = findpacs([dest])
+ if not src_pkg:
+ raise oscerr.NoWorkingCopy("Error: \"%s\" is not located in an osc working copy." % os.path.abspath(source))
+ if not tgt_pkg:
+ raise oscerr.NoWorkingCopy("Error: \"%s\" does not point to an osc working copy." % os.path.abspath(dest))
+
+ os.rename(source, dest)
+ try:
+ tgt_pkg[0].addfile(os.path.basename(dest))
+ except oscerr.PackageFileConflict:
+ # file is already tracked
+ pass
+ src_pkg[0].delete_file(os.path.basename(source), force=opts.force)
+
+ @cmdln.option('-d', '--delete', action='store_true',
+ help='delete option from config or reset option to the default)')
+ @cmdln.option('-s', '--stdin', action='store_true',
+ help='indicates that the config value should be read from stdin')
+ @cmdln.option('-p', '--prompt', action='store_true',
+ help='prompt for a value')
+ @cmdln.option('--no-echo', action='store_true',
+ help='prompt for a value but do not echo entered characters')
+ @cmdln.option('--dump', action='store_true',
+ help='dump the complete configuration (without \'pass\' and \'passx\' options)')
+ @cmdln.option('--dump-full', action='store_true',
+ help='dump the complete configuration (including \'pass\' and \'passx\' options)')
+ def do_config(self, subcmd, opts, *args):
+ """${cmd_name}: get/set a config option
+
+ Examples:
+ osc config section option (get current value)
+ osc config section option value (set to value)
+ osc config section option --delete (delete option/reset to the default)
+ (section is either an apiurl or an alias or 'general')
+ osc config --dump (dump the complete configuration)
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ if len(args) < 2 and not (opts.dump or opts.dump_full):
+ raise oscerr.WrongArgs('Too few arguments')
+ elif opts.dump or opts.dump_full:
+ cp = conf.get_configParser(conf.config['conffile'])
+ for sect in cp.sections():
+ print('[%s]' % sect)
+ for opt in sorted(cp.options(sect)):
+ if sect == 'general' and opt in conf.api_host_options or \
+ sect != 'general' and not opt in conf.api_host_options:
+ continue
+ if opt in ('pass', 'passx') and not opts.dump_full:
+ continue
+ val = str(cp.get(sect, opt, raw=True))
+ # special handling for continuation lines
+ val = '\n '.join(val.split('\n'))
+ print('%s = %s' % (opt, val))
+ print()
+ return
+
+ section, opt, val = args[0], args[1], args[2:]
+ if len(val) and (opts.delete or opts.stdin or opts.prompt or opts.no_echo):
+ raise oscerr.WrongOptions('Sorry, \'--delete\' or \'--stdin\' or \'--prompt\' or \'--no-echo\' ' \
+ 'and the specification of a value argument are mutually exclusive')
+ elif (opts.prompt or opts.no_echo) and opts.stdin:
+ raise oscerr.WrongOptions('Sorry, \'--prompt\' or \'--no-echo\' and \'--stdin\' are mutually exclusive')
+ elif opts.stdin:
+ # strip lines
+ val = [i.strip() for i in sys.stdin.readlines() if i.strip()]
+ if not len(val):
+ raise oscerr.WrongArgs('error: read empty value from stdin')
+ elif opts.no_echo or opts.prompt:
+ if opts.no_echo:
+ import getpass
+ inp = getpass.getpass('Value: ').strip()
+ else:
+ inp = raw_input('Value: ').strip()
+ if not inp:
+ raise oscerr.WrongArgs('error: no value was entered')
+ val = [inp]
+ opt, newval = conf.config_set_option(section, opt, ' '.join(val), delete=opts.delete, update=True)
+ if newval is None and opts.delete:
+ print('\'%s\': \'%s\' got removed' % (section, opt))
+ elif newval is None:
+ print('\'%s\': \'%s\' is not set' % (section, opt))
+ else:
+ if opts.no_echo:
+ # supress value
+ print('\'%s\': set \'%s\'' % (section, opt))
+ elif opt == 'pass' and not conf.config['plaintext_passwd'] and newval == 'your_password':
+ opt, newval = conf.config_set_option(section, 'passx')
+ print('\'%s\': \'pass\' was rewritten to \'passx\': \'%s\'' % (section, newval))
+ else:
+ print('\'%s\': \'%s\' is set to \'%s\'' % (section, opt, newval))
+
+ def do_revert(self, subcmd, opts, *files):
+ """${cmd_name}: Restore changed files or the entire working copy.
+
+ Examples:
+ osc revert <modified file(s)>
+ ose revert .
+ Note: this only works for package working copies
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ pacs = findpacs(files)
+ for p in pacs:
+ if not len(p.todo):
+ p.todo = p.filenamelist + p.to_be_added
+ for f in p.todo:
+ p.revert(f)
+
+ @cmdln.option('--force-apiurl', action='store_true',
+ help='ask once for an apiurl and force this apiurl for all inconsistent projects/packages')
+ def do_repairwc(self, subcmd, opts, *args):
+ """${cmd_name}: try to repair an inconsistent working copy
+
+ Examples:
+ osc repairwc <path>
+
+ Note: if <path> is omitted it defaults to '.' (<path> can be
+ a project or package working copy)
+
+ Warning: This command might delete some files in the storedir
+ (.osc). Please check the state of the wc afterwards (via 'osc status').
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ def get_apiurl(apiurls):
+ print('No apiurl is defined for this working copy.\n' \
+ 'Please choose one from the following list (enter the number):')
+ for i in range(len(apiurls)):
+ print(' %d) %s' % (i, apiurls[i]))
+ num = raw_input('> ')
+ try:
+ num = int(num)
+ except ValueError:
+ raise oscerr.WrongArgs('\'%s\' is not a number. Aborting' % num)
+ if num < 0 or num >= len(apiurls):
+ raise oscerr.WrongArgs('number \'%s\' out of range. Aborting' % num)
+ return apiurls[num]
+
+ args = parseargs(args)
+ pacs = []
+ apiurls = list(conf.config['api_host_options'].keys())
+ apiurl = ''
+ for i in args:
+ if is_project_dir(i):
+ try:
+ prj = Project(i, getPackageList=False)
+ except oscerr.WorkingCopyInconsistent as e:
+ if '_apiurl' in e.dirty_files and (not apiurl or not opts.force_apiurl):
+ apiurl = get_apiurl(apiurls)
+ prj = Project(i, getPackageList=False, wc_check=False)
+ prj.wc_repair(apiurl)
+ for p in prj.pacs_have:
+ if p in prj.pacs_broken:
+ continue
+ try:
+ Package(os.path.join(i, p))
+ except oscerr.WorkingCopyInconsistent:
+ pacs.append(os.path.join(i, p))
+ elif is_package_dir(i):
+ pacs.append(i)
+ else:
+ print('\'%s\' is neither a project working copy ' \
+ 'nor a package working copy' % i, file=sys.stderr)
+ for pdir in pacs:
+ try:
+ p = Package(pdir)
+ except oscerr.WorkingCopyInconsistent as e:
+ if '_apiurl' in e.dirty_files and (not apiurl or not opts.force_apiurl):
+ apiurl = get_apiurl(apiurls)
+ p = Package(pdir, wc_check=False)
+ p.wc_repair(apiurl)
+ print('done. Please check the state of the wc (via \'osc status %s\').' % i)
+ else:
+ print('osc: working copy \'%s\' is not inconsistent' % i, file=sys.stderr)
+
+ @cmdln.option('-n', '--dry-run', action='store_true',
+ help='print the results without actually removing a file')
+ def do_clean(self, subcmd, opts, *args):
+ """${cmd_name}: removes all untracked files from the package working copy
+
+ Examples:
+ osc clean <path>
+
+ Note: if <path> is omitted it defaults to '.' (<path> has to
+ be a package working copy)
+
+ Warning: This command removes all files with status '?'.
+
+ ${cmd_usage}
+ ${cmd_option_list}
+ """
+ pacs = parseargs(args)
+ # do a sanity check first
+ for pac in pacs:
+ if not is_package_dir(pac):
+ raise oscerr.WrongArgs('\'%s\' is no package working copy' % pac)
+ for pdir in pacs:
+ p = Package(pdir)
+ pdir = getTransActPath(pdir)
+ for filename in (fname for st, fname in p.get_status() if st == '?'):
+ print('Removing: %s' % os.path.join(pdir, filename))
+ if not opts.dry_run:
+ os.unlink(os.path.join(p.absdir, filename))
+
+ def _load_plugins(self):
+ plugin_dirs = [
+ '/usr/lib/osc-plugins',
+ '/usr/local/lib/osc-plugins',
+ '/var/lib/osc-plugins', # Kept for backward compatibility
+ os.path.expanduser('~/.osc-plugins')]
+ for plugin_dir in plugin_dirs:
+ if not os.path.isdir(plugin_dir):
+ continue
+ for extfile in os.listdir(plugin_dir):
+ if not extfile.endswith('.py'):
+ continue
+ try:
+ modname = os.path.splitext(extfile)[0]
+ mod = imp.load_source(modname, os.path.join(plugin_dir, extfile))
+ # restore the old exec semantic
+ mod.__dict__.update(globals())
+ for name in dir(mod):
+ data = getattr(mod, name)
+ # Add all functions (which are defined in the imported module)
+ # to the class (filtering only methods which start with "do_"
+ # breaks the old behavior).
+ # Also add imported modules (needed for backward compatibility).
+ # New plugins should not use "self.<imported modname>.<something>"
+ # to refer to the imported module. Instead use
+ # "<imported modname>.<something>".
+ if (inspect.isfunction(data) and inspect.getmodule(data) == mod
+ or inspect.ismodule(data)):
+ setattr(self.__class__, name, data)
+ except (SyntaxError, NameError, ImportError) as e:
+ if (os.environ.get('OSC_PLUGIN_FAIL_IGNORE')):
+ print("%s: %s\n" % (os.path.join(plugin_dir, extfile), e), file=sys.stderr)
+ else:
+ import traceback
+ traceback.print_exc(file=sys.stderr)
+ print('\n%s: %s' % (os.path.join(plugin_dir, extfile), e), file=sys.stderr)
+ print("\n Try 'env OSC_PLUGIN_FAIL_IGNORE=1 osc ...'", file=sys.stderr)
+ sys.exit(1)
+
+# fini!
+###############################################################################
+
+# vim: sw=4 et
diff --git a/osc/conf.py b/osc/conf.py
new file mode 100644
index 0000000..100259a
--- /dev/null
+++ b/osc/conf.py
@@ -0,0 +1,1031 @@
+# Copyright (C) 2006-2009 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or version 3 (at your option).
+
+from __future__ import print_function
+
+"""Read osc configuration and store it in a dictionary
+
+This module reads and parses ~/.oscrc. The resulting configuration is stored
+for later usage in a dictionary named 'config'.
+The .oscrc is kept mode 0600, so that it is not publically readable.
+This gives no real security for storing passwords.
+If in doubt, use your favourite keyring.
+Password is stored on ~/.oscrc as bz2 compressed and base64 encoded, so that is fairly
+large and not to be recognized or remembered easily by an occasional spectator.
+
+If information is missing, it asks the user questions.
+
+After reading the config, urllib2 is initialized.
+
+The configuration dictionary could look like this:
+
+{'apisrv': 'https://api.opensuse.org/',
+ 'user': 'joe',
+ 'api_host_options': {'api.opensuse.org': {'user': 'joe', 'pass': 'secret'},
+ 'apitest.opensuse.org': {'user': 'joe', 'pass': 'secret',
+ 'http_headers':(('Host','api.suse.de'),
+ ('User','faye'))},
+ 'foo.opensuse.org': {'user': 'foo', 'pass': 'foo'}},
+ 'build-cmd': '/usr/bin/build',
+ 'build-root': '/abuild/oscbuild-%(repo)s-%(arch)s',
+ 'packagecachedir': '/var/cache/osbuild',
+ 'su-wrapper': 'sudo',
+ }
+
+"""
+
+import bz2
+import base64
+import os
+import re
+import sys
+import ssl
+
+try:
+ from http.cookiejar import LWPCookieJar, CookieJar
+ from http.client import HTTPConnection, HTTPResponse
+ from io import StringIO
+ from urllib.parse import urlsplit
+ from urllib.error import URLError
+ from urllib.request import HTTPBasicAuthHandler, HTTPCookieProcessor, HTTPPasswordMgrWithDefaultRealm, ProxyHandler
+ from urllib.request import AbstractHTTPHandler, build_opener, proxy_bypass, HTTPSHandler
+except ImportError:
+ #python 2.x
+ from cookielib import LWPCookieJar, CookieJar
+ from httplib import HTTPConnection, HTTPResponse
+ from StringIO import StringIO
+ from urlparse import urlsplit
+ from urllib2 import URLError, HTTPBasicAuthHandler, HTTPCookieProcessor, HTTPPasswordMgrWithDefaultRealm, ProxyHandler
+ from urllib2 import AbstractHTTPHandler, build_opener, proxy_bypass, HTTPSHandler
+
+from . import OscConfigParser
+from osc import oscerr
+from .oscsslexcp import NoSecureSSLError
+
+GENERIC_KEYRING = False
+GNOME_KEYRING = False
+
+try:
+ import keyring
+ GENERIC_KEYRING = True
+except:
+ try:
+ import gobject
+ gobject.set_application_name('osc')
+ import gnomekeyring
+ if os.environ['GNOME_DESKTOP_SESSION_ID']:
+ # otherwise gnome keyring bindings spit out errors, when you have
+ # it installed, but you are not under gnome
+ # (even though hundreds of gnome-keyring daemons got started in parallel)
+ # another option would be to support kwallet here
+ GNOME_KEYRING = gnomekeyring.is_available()
+ except:
+ pass
+
+
+def _get_processors():
+ """
+ get number of processors (online) based on
+ SC_NPROCESSORS_ONLN (returns 1 if config name does not exist).
+ """
+ try:
+ return os.sysconf('SC_NPROCESSORS_ONLN')
+ except ValueError as e:
+ return 1
+
+DEFAULTS = {'apiurl': 'https://api.opensuse.org',
+ 'user': 'your_username',
+ 'pass': 'your_password',
+ 'passx': '',
+ 'packagecachedir': '/var/tmp/osbuild-packagecache',
+ 'su-wrapper': 'sudo',
+
+ # build type settings
+ 'build-cmd': '/usr/bin/build',
+ 'build-type': '', # may be empty for chroot, kvm or xen
+ 'build-root': '/var/tmp/build-root/%(repo)s-%(arch)s',
+ 'build-uid': '', # use the default provided by build
+ 'build-device': '', # required for VM builds
+ 'build-memory': '', # required for VM builds
+ 'build-swap': '', # optional for VM builds
+ 'build-vmdisk-rootsize': '', # optional for VM builds
+ 'build-vmdisk-swapsize': '', # optional for VM builds
+ 'build-vmdisk-filesystem': '', # optional for VM builds
+ 'build-vm-user': '', # optional for VM builds
+ 'build-kernel': '', # optional for VM builds
+ 'build-initrd': '', # optional for VM builds
+
+ 'build-jobs': _get_processors(),
+ 'builtin_signature_check': '1', # by default use builtin check for verify pkgs
+ 'icecream': '0',
+
+ 'buildlog_strip_time': '0', # strips the build time from the build log
+
+ 'debug': '0',
+ 'http_debug': '0',
+ 'http_full_debug': '0',
+ 'http_retries': '3',
+ 'verbose': '1',
+ 'traceback': '0',
+ 'post_mortem': '0',
+ 'use_keyring': '0',
+ 'gnome_keyring': '0',
+ 'cookiejar': '~/.osc_cookiejar',
+ # fallback for osc build option --no-verify
+ 'no_verify': '0',
+ # enable project tracking by default
+ 'do_package_tracking': '1',
+ # default for osc build
+ 'extra-pkgs': '',
+ # default repository
+ 'build_repository': 'openSUSE_Factory',
+ # default project for branch or bco
+ 'getpac_default_project': 'openSUSE:Factory',
+ # alternate filesystem layout: have multiple subdirs, where colons were.
+ 'checkout_no_colon': '0',
+ # change filesystem layout: avoid checkout from within a proj or package dir.
+ 'checkout_rooted': '0',
+ # local files to ignore with status, addremove, ....
+ 'exclude_glob': '.osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.vctmp.*',
+ # whether to keep passwords in plaintext.
+ 'plaintext_passwd': '1',
+ # limit the age of requests shown with 'osc req list'.
+ # this is a default only, can be overridden by 'osc req list -D NNN'
+ # Use 0 for unlimted.
+ 'request_list_days': 0,
+ # check for unversioned/removed files before commit
+ 'check_filelist': '1',
+ # check for pending requests after executing an action (e.g. checkout, update, commit)
+ 'check_for_request_on_action': '0',
+ # what to do with the source package if the submitrequest has been accepted
+ 'submitrequest_on_accept_action': '',
+ 'request_show_interactive': '0',
+ 'request_show_source_buildstatus': '0',
+ # if a review is accepted in interactive mode and a group
+ # was specified the review will be accepted for this group
+ 'review_inherit_group': '0',
+ 'submitrequest_accepted_template': '',
+ 'submitrequest_declined_template': '',
+ 'linkcontrol': '0',
+ 'include_request_from_project': '1',
+ 'local_service_run': '1',
+
+ # Maintenance defaults to OBS instance defaults
+ 'maintained_attribute': 'OBS:Maintained',
+ 'maintenance_attribute': 'OBS:MaintenanceProject',
+ 'maintained_update_project_attribute': 'OBS:UpdateProject',
+ 'show_download_progress': '0',
+ # path to the vc script
+ 'vc-cmd': '/usr/lib/build/vc'
+}
+
+# being global to this module, this dict can be accessed from outside
+# it will hold the parsed configuration
+config = DEFAULTS.copy()
+
+boolean_opts = ['debug', 'do_package_tracking', 'http_debug', 'post_mortem', 'traceback', 'check_filelist', 'plaintext_passwd',
+ 'checkout_no_colon', 'checkout_rooted', 'check_for_request_on_action', 'linkcontrol', 'show_download_progress', 'request_show_interactive',
+ 'request_show_source_buildstatus', 'review_inherit_group', 'use_keyring', 'gnome_keyring', 'no_verify', 'builtin_signature_check',
+ 'http_full_debug', 'include_request_from_project', 'local_service_run', 'buildlog_strip_time']
+
+api_host_options = ['user', 'pass', 'passx', 'aliases', 'http_headers', 'email', 'sslcertck', 'cafile', 'capath', 'trusted_prj']
+
+new_conf_template = """
+[general]
+
+# URL to access API server, e.g. %(apiurl)s
+# you also need a section [%(apiurl)s] with the credentials
+apiurl = %(apiurl)s
+
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = %(packagecachedir)s
+
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = %(su-wrapper)s
+
+# rootdir to setup the chroot environment
+# can contain %%(repo)s, %%(arch)s, %%(project)s, %%(package)s and %%(apihost)s (apihost is the hostname
+# extracted from currently used apiurl) for replacement, e.g.
+# /srv/oscbuild/%%(repo)s-%%(arch)s or
+# /srv/oscbuild/%%(repo)s-%%(arch)s-%%(project)s-%%(package)s
+#build-root = %(build-root)s
+
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+
+# build-kernel is the boot kernel used for VM builds
+#build-kernel = /boot/vmlinuz
+
+# build-initrd is the boot initrd used for VM builds
+#build-initrd = /boot/initrd
+
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+
+# build-vmdisk-filesystem is the file system type of the disk-image used in a VM build
+# values are ext3(default) ext4 xfs reiserfs btrfs
+#build-vmdisk-filesystem = ext4
+
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+
+# strip leading build time information from the build log
+# buildlog_strip_time = 1
+
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = %(build_repository)s
+
+# default project for getpac or bco
+#getpac_default_project = %(getpac_default_project)s
+
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = %(checkout_no_colon)s
+
+# change filesystem layout: avoid checkout within a project or package dir.
+#checkout_rooted = %(checkout_rooted)s
+
+# local files to ignore with status, addremove, ....
+#exclude_glob = %(exclude_glob)s
+
+# keep passwords in plaintext.
+# Set to 0 to obfuscate passwords. It's no real security, just
+# prevents most people from remembering your password if they watch
+# you editing this file.
+#plaintext_passwd = %(plaintext_passwd)s
+
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = %(request_list_days)s
+
+# show info useful for debugging
+#debug = 1
+
+# show HTTP traffic useful for debugging
+#http_debug = 1
+
+# number of retries on HTTP transfer
+#http_retries = 3
+
+# Skip signature verification of packages used for build.
+#no_verify = 1
+
+# jump into the debugger in case of errors
+#post_mortem = 1
+
+# print call traces in case of errors
+#traceback = 1
+
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+
+# check for unversioned/removed files before commit
+#check_filelist = 1
+
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+
+# template for an accepted submitrequest
+#submitrequest_accepted_template = Hi %%(who)s,\\n
+# thanks for working on:\\t%%(tgt_project)s/%%(tgt_package)s.
+# SR %%(reqid)s has been accepted.\\n\\nYour maintainers
+
+# template for a declined submitrequest
+#submitrequest_declined_template = Hi %%(who)s,\\n
+# sorry your SR %%(reqid)s (request type: %%(type)s) for
+# %%(tgt_project)s/%%(tgt_package)s has been declined because...
+
+#review requests interactively (default: off)
+#request_show_review = 1
+
+# if a review is accepted in interactive mode and a group
+# was specified the review will be accepted for this group (default: off)
+#review_inherit_group = 1
+
+[%(apiurl)s]
+user = %(user)s
+pass = %(pass)s
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Plain text password
+#pass =
+# Force using of keyring for this API
+#keyring = 1
+"""
+
+
+account_not_configured_text = """
+Your user account / password are not configured yet.
+You will be asked for them below, and they will be stored in
+%s for future use.
+"""
+
+config_incomplete_text = """
+
+Your configuration file %s is not complete.
+Make sure that it has a [general] section.
+(You can copy&paste the below. Some commented defaults are shown.)
+
+"""
+
+config_missing_apiurl_text = """
+the apiurl \'%s\' does not exist in the config file. Please enter
+your credentials for this apiurl.
+"""
+
+cookiejar = None
+
+
+def parse_apisrv_url(scheme, apisrv):
+ if apisrv.startswith('http://') or apisrv.startswith('https://'):
+ url = apisrv
+ elif scheme != None:
+ url = scheme + apisrv
+ else:
+ msg = 'invalid apiurl \'%s\' (specify the protocol (http:// or https://))' % apisrv
+ raise URLError(msg)
+ scheme, url, path = urlsplit(url)[0:3]
+ return scheme, url, path.rstrip('/')
+
+
+def urljoin(scheme, apisrv, path=''):
+ return '://'.join([scheme, apisrv]) + path
+
+
+def is_known_apiurl(url):
+ """returns true if url is a known apiurl"""
+ apiurl = urljoin(*parse_apisrv_url(None, url))
+ return apiurl in config['api_host_options']
+
+
+def extract_known_apiurl(url):
+ """
+ Return longest prefix of given url that is known apiurl,
+ None if there is no known apiurl that is prefix of given url.
+ """
+ scheme, host, path = parse_apisrv_url(None, url)
+ p = path.split('/')
+ while p:
+ apiurl = urljoin(scheme, host, '/'.join(p))
+ if apiurl in config['api_host_options']:
+ return apiurl
+ p.pop()
+ return None
+
+
+def get_apiurl_api_host_options(apiurl):
+ """
+ Returns all apihost specific options for the given apiurl, None if
+ no such specific optiosn exist.
+ """
+ # FIXME: in A Better World (tm) there was a config object which
+ # knows this instead of having to extract it from a url where it
+ # had been mingled into before. But this works fine for now.
+
+ apiurl = urljoin(*parse_apisrv_url(None, apiurl))
+ if is_known_apiurl(apiurl):
+ return config['api_host_options'][apiurl]
+ raise oscerr.ConfigMissingApiurl('missing credentials for apiurl: \'%s\'' % apiurl,
+ '', apiurl)
+
+
+def get_apiurl_usr(apiurl):
+ """
+ returns the user for this host - if this host does not exist in the
+ internal api_host_options the default user is returned.
+ """
+ # FIXME: maybe there should be defaults not just for the user but
+ # for all apihost specific options. The ConfigParser class
+ # actually even does this but for some reason we don't use it
+ # (yet?).
+
+ try:
+ return get_apiurl_api_host_options(apiurl)['user']
+ except KeyError:
+ print('no specific section found in config file for host of [\'%s\'] - using default user: \'%s\'' \
+ % (apiurl, config['user']), file=sys.stderr)
+ return config['user']
+
+
+# workaround m2crypto issue:
+# if multiple SSL.Context objects are created
+# m2crypto only uses the last object which was created.
+# So we need to build a new opener everytime we switch the
+# apiurl (because different apiurls may have different
+# cafile/capath locations)
+def _build_opener(apiurl):
+ from osc.core import __version__
+ global config
+ if 'last_opener' not in _build_opener.__dict__:
+ _build_opener.last_opener = (None, None)
+ if apiurl == _build_opener.last_opener[0]:
+ return _build_opener.last_opener[1]
+
+ # respect no_proxy env variable
+ if proxy_bypass(apiurl):
+ # initialize with empty dict
+ proxyhandler = ProxyHandler({})
+ else:
+ # read proxies from env
+ proxyhandler = ProxyHandler()
+
+ # workaround for http://bugs.python.org/issue9639
+ authhandler_class = HTTPBasicAuthHandler
+ if sys.version_info >= (2, 6, 6) and sys.version_info < (2, 7, 1) \
+ and not 'reset_retry_count' in dir(HTTPBasicAuthHandler):
+ print('warning: your urllib2 version seems to be broken. ' \
+ 'Using a workaround for http://bugs.python.org/issue9639', file=sys.stderr)
+
+ class OscHTTPBasicAuthHandler(HTTPBasicAuthHandler):
+ def http_error_401(self, *args):
+ response = HTTPBasicAuthHandler.http_error_401(self, *args)
+ self.retried = 0
+ return response
+
+ def http_error_404(self, *args):
+ self.retried = 0
+ return None
+
+ authhandler_class = OscHTTPBasicAuthHandler
+ elif sys.version_info >= (2, 6, 6) and sys.version_info < (2, 7, 1):
+ class OscHTTPBasicAuthHandler(HTTPBasicAuthHandler):
+ def http_error_404(self, *args):
+ self.reset_retry_count()
+ return None
+
+ authhandler_class = OscHTTPBasicAuthHandler
+ elif sys.version_info >= (2, 6, 5) and sys.version_info < (2, 6, 6):
+ # workaround for broken urllib2 in python 2.6.5: wrong credentials
+ # lead to an infinite recursion
+ class OscHTTPBasicAuthHandler(HTTPBasicAuthHandler):
+ def retry_http_basic_auth(self, host, req, realm):
+ # don't retry if auth failed
+ if req.get_header(self.auth_header, None) is not None:
+ return None
+ return HTTPBasicAuthHandler.retry_http_basic_auth(self, host, req, realm)
+
+ authhandler_class = OscHTTPBasicAuthHandler
+
+ options = config['api_host_options'][apiurl]
+ # with None as first argument, it will always use this username/password
+ # combination for urls for which arg2 (apisrv) is a super-url
+ authhandler = authhandler_class( \
+ HTTPPasswordMgrWithDefaultRealm())
+ authhandler.add_password(None, apiurl, options['user'], options['pass'])
+
+ if options['sslcertck']:
+ try:
+ from . import oscssl
+ from M2Crypto import m2urllib2
+ except ImportError as e:
+ print(e)
+ raise NoSecureSSLError('M2Crypto is needed to access %s in a secure way.\nPlease install python-m2crypto.' % apiurl)
+
+ cafile = options.get('cafile', None)
+ capath = options.get('capath', None)
+ if not cafile and not capath:
+ for i in ['/etc/pki/tls/cert.pem', '/etc/ssl/certs']:
+ if os.path.isfile(i):
+ cafile = i
+ break
+ elif os.path.isdir(i):
+ capath = i
+ break
+ if not cafile and not capath:
+ raise oscerr.OscIOError(None, 'No CA certificates found')
+ ctx = oscssl.mySSLContext()
+ if ctx.load_verify_locations(capath=capath, cafile=cafile) != 1:
+ raise oscerr.OscIOError(None, 'No CA certificates found')
+ opener = m2urllib2.build_opener(ctx, oscssl.myHTTPSHandler(ssl_context=ctx, appname='osc'), HTTPCookieProcessor(cookiejar), authhandler, proxyhandler)
+ else:
+ handlers = [HTTPCookieProcessor(cookiejar), authhandler, proxyhandler]
+ try:
+ # disable ssl cert check in python >= 2.7.9
+ ctx = ssl._create_unverified_context()
+ handlers.append(HTTPSHandler(context=ctx))
+ except AttributeError:
+ pass
+ print("WARNING: SSL certificate checks disabled. Connection is insecure!\n", file=sys.stderr)
+ opener = build_opener(*handlers)
+ opener.addheaders = [('User-agent', 'osc/%s' % __version__)]
+ _build_opener.last_opener = (apiurl, opener)
+ return opener
+
+
+def init_basicauth(config):
+ """initialize urllib2 with the credentials for Basic Authentication"""
+
+ def filterhdrs(meth, ishdr, *hdrs):
+ # this is so ugly but httplib doesn't use
+ # a logger object or such
+ def new_method(self, *args, **kwargs):
+ # check if this is a recursive call (note: we do not
+ # have to care about thread safety)
+ is_rec_call = getattr(self, '_orig_stdout', None) is not None
+ try:
+ if not is_rec_call:
+ self._orig_stdout = sys.stdout
+ sys.stdout = StringIO()
+ meth(self, *args, **kwargs)
+ hdr = sys.stdout.getvalue()
+ finally:
+ # restore original stdout
+ if not is_rec_call:
+ sys.stdout = self._orig_stdout
+ del self._orig_stdout
+ for i in hdrs:
+ if ishdr:
+ hdr = re.sub(r'%s:[^\\r]*\\r\\n' % i, '', hdr)
+ else:
+ hdr = re.sub(i, '', hdr)
+ sys.stdout.write(hdr)
+ new_method.__name__ = meth.__name__
+ return new_method
+
+ if config['http_debug'] and not config['http_full_debug']:
+ HTTPConnection.send = filterhdrs(HTTPConnection.send, True, 'Cookie', 'Authorization')
+ HTTPResponse.begin = filterhdrs(HTTPResponse.begin, False, 'header: Set-Cookie.*\n')
+
+ if sys.version_info < (2, 6):
+ # HTTPS proxy is not supported in old urllib2. It only leads to an error
+ # or, at best, a warning.
+ if 'https_proxy' in os.environ:
+ del os.environ['https_proxy']
+ if 'HTTPS_PROXY' in os.environ:
+ del os.environ['HTTPS_PROXY']
+
+ if config['http_debug']:
+ # brute force
+ def urllib2_debug_init(self, debuglevel=0):
+ self._debuglevel = 1
+ AbstractHTTPHandler.__init__ = urllib2_debug_init
+
+ cookie_file = os.path.expanduser(config['cookiejar'])
+ global cookiejar
+ cookiejar = LWPCookieJar(cookie_file)
+ try:
+ cookiejar.load(ignore_discard=True)
+ except IOError:
+ try:
+ fd = os.open(cookie_file, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
+ os.close(fd)
+ except IOError:
+ # hmm is any good reason why we should catch the IOError?
+ #print 'Unable to create cookiejar file: \'%s\'. Using RAM-based cookies.' % cookie_file
+ cookiejar = CookieJar()
+
+
+def get_configParser(conffile=None, force_read=False):
+ """
+ Returns an ConfigParser() object. After its first invocation the
+ ConfigParser object is stored in a method attribute and this attribute
+ is returned unless you pass force_read=True.
+ """
+ conffile = conffile or os.environ.get('OSC_CONFIG', '~/.oscrc')
+ conffile = os.path.expanduser(conffile)
+ if 'conffile' not in get_configParser.__dict__:
+ get_configParser.conffile = conffile
+ if force_read or 'cp' not in get_configParser.__dict__ or conffile != get_configParser.conffile:
+ get_configParser.cp = OscConfigParser.OscConfigParser(DEFAULTS)
+ get_configParser.cp.read(conffile)
+ get_configParser.conffile = conffile
+ return get_configParser.cp
+
+
+def write_config(fname, cp):
+ """write new configfile in a safe way"""
+ if os.path.exists(fname) and not os.path.isfile(fname):
+ # only write to a regular file
+ return
+ with open(fname + '.new', 'w') as f:
+ cp.write(f, comments=True)
+ try:
+ os.rename(fname + '.new', fname)
+ os.chmod(fname, 0o600)
+ except:
+ if os.path.exists(fname + '.new'):
+ os.unlink(fname + '.new')
+ raise
+
+
+def config_set_option(section, opt, val=None, delete=False, update=True, **kwargs):
+ """
+ Sets a config option. If val is not specified the current/default value is
+ returned. If val is specified, opt is set to val and the new value is returned.
+ If an option was modified get_config is called with **kwargs unless update is set
+ to False (override_conffile defaults to config['conffile']).
+ If val is not specified and delete is True then the option is removed from the
+ config/reset to the default value.
+ """
+ cp = get_configParser(config['conffile'])
+ # don't allow "internal" options
+ general_opts = [i for i in DEFAULTS.keys() if not i in ['user', 'pass', 'passx']]
+ if section != 'general':
+ section = config['apiurl_aliases'].get(section, section)
+ scheme, host, path = \
+ parse_apisrv_url(config.get('scheme', 'https'), section)
+ section = urljoin(scheme, host, path)
+
+ sections = {}
+ for url in cp.sections():
+ if url == 'general':
+ sections[url] = url
+ else:
+ scheme, host, path = \
+ parse_apisrv_url(config.get('scheme', 'https'), url)
+ apiurl = urljoin(scheme, host, path)
+ sections[apiurl] = url
+
+ section = sections.get(section.rstrip('/'), section)
+ if not section in cp.sections():
+ raise oscerr.ConfigError('unknown section \'%s\'' % section, config['conffile'])
+ if section == 'general' and not opt in general_opts or \
+ section != 'general' and not opt in api_host_options:
+ raise oscerr.ConfigError('unknown config option \'%s\'' % opt, config['conffile'])
+ run = False
+ if val:
+ cp.set(section, opt, val)
+ write_config(config['conffile'], cp)
+ run = True
+ elif delete and cp.has_option(section, opt):
+ cp.remove_option(section, opt)
+ write_config(config['conffile'], cp)
+ run = True
+ if run and update:
+ kw = {'override_conffile': config['conffile'],
+ 'override_no_keyring': config['use_keyring'],
+ 'override_no_gnome_keyring': config['gnome_keyring']}
+ kw.update(kwargs)
+ get_config(**kw)
+ if cp.has_option(section, opt):
+ return (opt, cp.get(section, opt, raw=True))
+ return (opt, None)
+
+def passx_decode(passx):
+ """decode the obfuscated password back to plain text password"""
+ return bz2.decompress(base64.b64decode(passx.encode("ascii"))).decode("ascii")
+
+def passx_encode(passwd):
+ """encode plain text password to obfuscated form"""
+ return base64.b64encode(bz2.compress(passwd.encode('ascii'))).decode("ascii")
+
+def write_initial_config(conffile, entries, custom_template=''):
+ """
+ write osc's intial configuration file. entries is a dict which contains values
+ for the config file (e.g. { 'user' : 'username', 'pass' : 'password' } ).
+ custom_template is an optional configuration template.
+ """
+ conf_template = custom_template or new_conf_template
+ config = DEFAULTS.copy()
+ config.update(entries)
+ # at this point use_keyring and gnome_keyring are str objects
+ if config['use_keyring'] == '1' and GENERIC_KEYRING:
+ protocol, host, path = \
+ parse_apisrv_url(None, config['apiurl'])
+ keyring.set_password(host, config['user'], config['pass'])
+ config['pass'] = ''
+ config['passx'] = ''
+ elif config['gnome_keyring'] == '1' and GNOME_KEYRING:
+ protocol, host, path = \
+ parse_apisrv_url(None, config['apiurl'])
+ gnomekeyring.set_network_password_sync(
+ user=config['user'],
+ password=config['pass'],
+ protocol=protocol,
+ server=host,
+ object=path)
+ config['user'] = ''
+ config['pass'] = ''
+ config['passx'] = ''
+ if not config['plaintext_passwd']:
+ config['pass'] = ''
+ else:
+ config['passx'] = passx_encode(config['pass'])
+
+ sio = StringIO(conf_template.strip() % config)
+ cp = OscConfigParser.OscConfigParser(DEFAULTS)
+ cp.readfp(sio)
+ write_config(conffile, cp)
+
+
+def add_section(filename, url, user, passwd):
+ """
+ Add a section to config file for new api url.
+ """
+ global config
+ cp = get_configParser(filename)
+ try:
+ cp.add_section(url)
+ except OscConfigParser.configparser.DuplicateSectionError:
+ # Section might have existed, but was empty
+ pass
+ if config['use_keyring'] and GENERIC_KEYRING:
+ protocol, host, path = parse_apisrv_url(None, url)
+ keyring.set_password(host, user, passwd)
+ cp.set(url, 'keyring', '1')
+ cp.set(url, 'user', user)
+ cp.remove_option(url, 'pass')
+ cp.remove_option(url, 'passx')
+ elif config['gnome_keyring'] and GNOME_KEYRING:
+ protocol, host, path = parse_apisrv_url(None, url)
+ gnomekeyring.set_network_password_sync(
+ user=user,
+ password=passwd,
+ protocol=protocol,
+ server=host,
+ object=path)
+ cp.set(url, 'keyring', '1')
+ cp.remove_option(url, 'pass')
+ cp.remove_option(url, 'passx')
+ else:
+ cp.set(url, 'user', user)
+ if not config['plaintext_passwd']:
+ cp.remove_option(url, 'pass')
+ cp.set(url, 'passx', passx_encode(passwd))
+ else:
+ cp.remove_option(url, 'passx')
+ cp.set(url, 'pass', passwd)
+ write_config(filename, cp)
+
+
+def get_config(override_conffile=None,
+ override_apiurl=None,
+ override_debug=None,
+ override_http_debug=None,
+ override_http_full_debug=None,
+ override_traceback=None,
+ override_post_mortem=None,
+ override_no_keyring=None,
+ override_no_gnome_keyring=None,
+ override_verbose=None):
+ """do the actual work (see module documentation)"""
+ global config
+
+ conffile = override_conffile or os.environ.get('OSC_CONFIG', '~/.oscrc')
+ conffile = os.path.expanduser(conffile)
+
+ if not os.path.exists(conffile):
+ raise oscerr.NoConfigfile(conffile, \
+ account_not_configured_text % conffile)
+
+ # okay, we made sure that .oscrc exists
+
+ # make sure it is not world readable, it may contain a password.
+ os.chmod(conffile, 0o600)
+
+ cp = get_configParser(conffile)
+
+ if not cp.has_section('general'):
+ # FIXME: it might be sufficient to just assume defaults?
+ msg = config_incomplete_text % conffile
+ msg += new_conf_template % DEFAULTS
+ raise oscerr.ConfigError(msg, conffile)
+
+ config = dict(cp.items('general', raw=1))
+ config['conffile'] = conffile
+
+ for i in boolean_opts:
+ try:
+ config[i] = cp.getboolean('general', i)
+ except ValueError as e:
+ raise oscerr.ConfigError('cannot parse \'%s\' setting: ' % i + str(e), conffile)
+
+ config['packagecachedir'] = os.path.expanduser(config['packagecachedir'])
+ config['exclude_glob'] = config['exclude_glob'].split()
+
+ re_clist = re.compile('[, ]+')
+ config['extra-pkgs'] = [i.strip() for i in re_clist.split(config['extra-pkgs'].strip()) if i]
+
+ # collect the usernames, passwords and additional options for each api host
+ api_host_options = {}
+
+ # Regexp to split extra http headers into a dictionary
+ # the text to be matched looks essentially looks this:
+ # "Attribute1: value1, Attribute2: value2, ..."
+ # there may be arbitray leading and intermitting whitespace.
+ # the following regexp does _not_ support quoted commas within the value.
+ http_header_regexp = re.compile(r"\s*(.*?)\s*:\s*(.*?)\s*(?:,\s*|\Z)")
+
+ # override values which we were called with
+ # This needs to be done before processing API sections as it might be already used there
+ if override_no_keyring:
+ config['use_keyring'] = False
+ if override_no_gnome_keyring:
+ config['gnome_keyring'] = False
+
+ aliases = {}
+ for url in [x for x in cp.sections() if x != 'general']:
+ # backward compatiblity
+ scheme, host, path = parse_apisrv_url(config.get('scheme', 'https'), url)
+ apiurl = urljoin(scheme, host, path)
+ user = None
+ password = None
+ if config['use_keyring'] and GENERIC_KEYRING:
+ try:
+ # Read from keyring lib if available
+ user = cp.get(url, 'user', raw=True)
+ password = str(keyring.get_password(host, user))
+ except:
+ # Fallback to file based auth.
+ pass
+ elif config['gnome_keyring'] and GNOME_KEYRING:
+ # Read from gnome keyring if available
+ try:
+ gk_data = gnomekeyring.find_network_password_sync(protocol=scheme, server=host, object=path)
+ if not 'user' in gk_data[0]:
+ raise oscerr.ConfigError('no user found in keyring', conffile)
+ user = gk_data[0]['user']
+ if 'password' in gk_data[0]:
+ password = str(gk_data[0]['password'])
+ else:
+ # this is most likely an error
+ print('warning: no password found in keyring', file=sys.stderr)
+ except gnomekeyring.NoMatchError:
+ # Fallback to file based auth.
+ pass
+
+ if not user is None and len(user) == 0:
+ user = None
+ print('Warning: blank user in the keyring for the ' \
+ 'apiurl %s.\nPlease fix your keyring entry.', file=sys.stderr)
+
+ if user is not None and password is None:
+ err = ('no password defined for "%s".\nPlease fix your keyring '
+ 'entry or gnome-keyring setup.\nAssuming an empty password.'
+ % url)
+ print(err, file=sys.stderr)
+ password = ''
+
+ # Read credentials from config
+ if user is None:
+ #FIXME: this could actually be the ideal spot to take defaults
+ #from the general section.
+ user = cp.get(url, 'user', raw=True) # need to set raw to prevent '%' expansion
+ password = cp.get(url, 'pass', raw=True) # especially on password!
+ try:
+ passwordx = passx_decode(cp.get(url, 'passx', raw=True)) # especially on password!
+ except:
+ passwordx = ''
+
+ if password == None or password == 'your_password':
+ password = ''
+
+ if user is None or user == '':
+ raise oscerr.ConfigError('user is blank for %s, please delete or complete the "user=" entry in %s.' % (apiurl, config['conffile']), config['conffile'])
+
+ if config['plaintext_passwd'] and passwordx or not config['plaintext_passwd'] and password:
+ if config['plaintext_passwd']:
+ if password != passwordx:
+ print('%s: rewriting from encoded pass to plain pass' % url, file=sys.stderr)
+ add_section(conffile, url, user, passwordx)
+ password = passwordx
+ else:
+ if password != passwordx:
+ print('%s: rewriting from plain pass to encoded pass' % url, file=sys.stderr)
+ add_section(conffile, url, user, password)
+
+ if not config['plaintext_passwd']:
+ password = passwordx
+
+ if cp.has_option(url, 'http_headers'):
+ http_headers = cp.get(url, 'http_headers')
+ http_headers = http_header_regexp.findall(http_headers)
+ else:
+ http_headers = []
+ if cp.has_option(url, 'aliases'):
+ for i in cp.get(url, 'aliases').split(','):
+ key = i.strip()
+ if key == '':
+ continue
+ if key in aliases:
+ msg = 'duplicate alias entry: \'%s\' is already used for another apiurl' % key
+ raise oscerr.ConfigError(msg, conffile)
+ aliases[key] = url
+
+ api_host_options[apiurl] = {'user': user,
+ 'pass': password,
+ 'http_headers': http_headers}
+
+ optional = ('email', 'sslcertck', 'cafile', 'capath')
+ for key in optional:
+ if cp.has_option(url, key):
+ if key == 'sslcertck':
+ api_host_options[apiurl][key] = cp.getboolean(url, key)
+ else:
+ api_host_options[apiurl][key] = cp.get(url, key)
+ if cp.has_option(url, 'build-root', proper=True):
+ api_host_options[apiurl]['build-root'] = cp.get(url, 'build-root', raw=True)
+
+ if not 'sslcertck' in api_host_options[apiurl]:
+ api_host_options[apiurl]['sslcertck'] = True
+
+ if scheme == 'http':
+ api_host_options[apiurl]['sslcertck'] = False
+
+ if cp.has_option(url, 'trusted_prj'):
+ api_host_options[apiurl]['trusted_prj'] = cp.get(url, 'trusted_prj').split(' ')
+ else:
+ api_host_options[apiurl]['trusted_prj'] = []
+
+ # add the auth data we collected to the config dict
+ config['api_host_options'] = api_host_options
+ config['apiurl_aliases'] = aliases
+
+ apiurl = aliases.get(config['apiurl'], config['apiurl'])
+ config['apiurl'] = urljoin(*parse_apisrv_url(None, apiurl))
+ # backward compatibility
+ if 'apisrv' in config:
+ apisrv = config['apisrv'].lstrip('http://')
+ apisrv = apisrv.lstrip('https://')
+ scheme = config.get('scheme', 'https')
+ config['apiurl'] = urljoin(scheme, apisrv)
+ if 'apisrc' in config or 'scheme' in config:
+ print('Warning: Use of the \'scheme\' or \'apisrv\' in ~/.oscrc is deprecated!\n' \
+ 'Warning: See README for migration details.', file=sys.stderr)
+ if 'build_platform' in config:
+ print('Warning: Use of \'build_platform\' config option is deprecated! (use \'build_repository\' instead)', file=sys.stderr)
+ config['build_repository'] = config['build_platform']
+
+ config['verbose'] = int(config['verbose'])
+ # override values which we were called with
+ if override_verbose:
+ config['verbose'] = override_verbose + 1
+
+ if override_debug:
+ config['debug'] = override_debug
+ if override_http_debug:
+ config['http_debug'] = override_http_debug
+ if override_http_full_debug:
+ config['http_debug'] = override_http_full_debug or config['http_debug']
+ config['http_full_debug'] = override_http_full_debug
+ if override_traceback:
+ config['traceback'] = override_traceback
+ if override_post_mortem:
+ config['post_mortem'] = override_post_mortem
+ if override_apiurl:
+ apiurl = aliases.get(override_apiurl, override_apiurl)
+ # check if apiurl is a valid url
+ config['apiurl'] = urljoin(*parse_apisrv_url(None, apiurl))
+
+ # XXX unless config['user'] goes away (and is replaced with a handy function, or
+ # config becomes an object, even better), set the global 'user' here as well,
+ # provided that there _are_ credentials for the chosen apiurl:
+ try:
+ config['user'] = get_apiurl_usr(config['apiurl'])
+ except oscerr.ConfigMissingApiurl as e:
+ e.msg = config_missing_apiurl_text % config['apiurl']
+ e.file = conffile
+ raise e
+
+ # finally, initialize urllib2 for to use the credentials for Basic Authentication
+ init_basicauth(config)
+
+
+# vim: sw=4 et
diff --git a/osc/core.py b/osc/core.py
new file mode 100644
index 0000000..3ee2c56
--- /dev/null
+++ b/osc/core.py
@@ -0,0 +1,7281 @@
+# Copyright (C) 2006 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or version 3 (at your option).
+
+from __future__ import print_function
+
+__version__ = '0.154.0'
+
+# __store_version__ is to be incremented when the format of the working copy
+# "store" changes in an incompatible way. Please add any needed migration
+# functionality to check_store_version().
+__store_version__ = '1.0'
+
+import locale
+import os
+import os.path
+import sys
+import shutil
+import subprocess
+import re
+import socket
+import errno
+import shlex
+
+try:
+ from urllib.parse import urlsplit, urlunsplit, urlparse, quote_plus, urlencode, unquote
+ from urllib.error import HTTPError
+ from urllib.request import pathname2url, install_opener, urlopen
+ from urllib.request import Request as URLRequest
+ from io import StringIO
+except ImportError:
+ #python 2.x
+ from urlparse import urlsplit, urlunsplit, urlparse
+ from urllib import pathname2url, quote_plus, urlencode, unquote
+ from urllib2 import HTTPError, install_opener, urlopen
+ from urllib2 import Request as URLRequest
+ from cStringIO import StringIO
+
+
+try:
+ from xml.etree import cElementTree as ET
+except ImportError:
+ import cElementTree as ET
+
+from . import oscerr
+from . import conf
+
+try:
+ # python 2.6 and python 2.7
+ unicode
+ ET_ENCODING = "utf-8"
+ # python 2.6 does not have bytes and python 2.7 reimplements it as alias to
+ # str, but in incompatible way as it does not accept the same arguments
+ bytes = lambda x, *args: x
+except:
+ #python3 does not have unicode, so lets reimplement it
+ #as void function as it already gets unicode strings
+ unicode = lambda x, *args: x
+ ET_ENCODING = "unicode"
+
+DISTURL_RE = re.compile(r"^(?P<bs>.*)://(?P<apiurl>.*?)/(?P<project>.*?)/(?P<repository>.*?)/(?P<revision>.*)-(?P<source>.*)$")
+BUILDLOGURL_RE = re.compile(r"^(?P<apiurl>https?://.*?)/build/(?P<project>.*?)/(?P<repository>.*?)/(?P<arch>.*?)/(?P<package>.*?)/_log$")
+BUFSIZE = 1024*1024
+store = '.osc'
+
+new_project_templ = """\
+<project name="%(name)s">
+
+ <title></title> <!-- Short title of NewProject -->
+ <description></description>
+ <!-- This is for a longer description of the purpose of the project -->
+
+ <person role="maintainer" userid="%(user)s" />
+ <person role="bugowner" userid="%(user)s" />
+<!-- remove this block to publish your packages on the mirrors -->
+ <publish>
+ <disable />
+ </publish>
+ <build>
+ <enable />
+ </build>
+ <debuginfo>
+ <enable />
+ </debuginfo>
+
+<!-- remove this comment to enable one or more build targets
+
+ <repository name="openSUSE_Factory">
+ <path project="openSUSE:Factory" repository="snapshot" />
+ <arch>x86_64</arch>
+ <arch>i586</arch>
+ </repository>
+ <repository name="openSUSE_13.2">
+ <path project="openSUSE:13.2" repository="standard"/>
+ <arch>x86_64</arch>
+ <arch>i586</arch>
+ </repository>
+ <repository name="openSUSE_13.1">
+ <path project="openSUSE:13.1" repository="standard"/>
+ <arch>x86_64</arch>
+ <arch>i586</arch>
+ </repository>
+ <repository name="Fedora_21">
+ <path project="Fedora:21" repository="standard" />
+ <arch>x86_64</arch>
+ <arch>i586</arch>
+ </repository>
+ <repository name="SLE_12">
+ <path project="SUSE:SLE-12:GA" repository="standard" />
+ <arch>x86_64</arch>
+ <arch>i586</arch>
+ </repository>
+-->
+
+</project>
+"""
+
+new_package_templ = """\
+<package name="%(name)s">
+
+ <title></title> <!-- Title of package -->
+
+ <description></description> <!-- for long description -->
+
+<!-- following roles are inherited from the parent project
+ <person role="maintainer" userid="%(user)s"/>
+ <person role="bugowner" userid="%(user)s"/>
+-->
+<!--
+ <url>PUT_UPSTREAM_URL_HERE</url>
+-->
+
+<!--
+ use one of the examples below to disable building of this package
+ on a certain architecture, in a certain repository,
+ or a combination thereof:
+
+ <disable arch="x86_64"/>
+ <disable repository="SUSE_SLE-10"/>
+ <disable repository="SUSE_SLE-10" arch="x86_64"/>
+
+ Possible sections where you can use the tags above:
+ <build>
+ </build>
+ <debuginfo>
+ </debuginfo>
+ <publish>
+ </publish>
+ <useforbuild>
+ </useforbuild>
+
+ Please have a look at:
+ http://en.opensuse.org/Restricted_formats
+ Packages containing formats listed there are NOT allowed to
+ be packaged in the openSUSE Buildservice and will be deleted!
+
+-->
+
+</package>
+"""
+
+new_attribute_templ = """\
+<attributes>
+ <attribute namespace="" name="">
+ <value><value>
+ </attribute>
+</attributes>
+"""
+
+new_user_template = """\
+<person>
+ <login>%(user)s</login>
+ <email>PUT_EMAIL_ADDRESS_HERE</email>
+ <realname>PUT_REAL_NAME_HERE</realname>
+ <watchlist>
+ <project name="home:%(user)s"/>
+ </watchlist>
+</person>
+"""
+
+info_templ = """\
+Project name: %s
+Package name: %s
+Path: %s
+API URL: %s
+Source URL: %s
+srcmd5: %s
+Revision: %s
+Link info: %s
+"""
+
+new_pattern_template = """\
+<!-- See https://github.com/openSUSE/libzypp/tree/master/zypp/parser/yum/schema/patterns.rng -->
+
+<!--
+<pattern xmlns="http://novell.com/package/metadata/suse/pattern"
+ xmlns:rpm="http://linux.duke.edu/metadata/rpm">
+ <name></name>
+ <summary></summary>
+ <description></description>
+ <uservisible/>
+ <category lang="en"></category>
+ <rpm:requires>
+ <rpm:entry name="must-have-package"/>
+ </rpm:requires>
+ <rpm:recommends>
+ <rpm:entry name="package"/>
+ </rpm:recommends>
+ <rpm:suggests>
+ <rpm:entry name="anotherpackage"/>
+ </rpm:suggests>
+</pattern>
+-->
+"""
+
+buildstatus_symbols = {'succeeded': '.',
+ 'disabled': ' ',
+ 'expansion error': 'U', # obsolete with OBS 2.0
+ 'unresolvable': 'U',
+ 'failed': 'F',
+ 'broken': 'B',
+ 'blocked': 'b',
+ 'building': '%',
+ 'finished': 'f',
+ 'scheduled': 's',
+ 'locked': 'L',
+ 'excluded': 'x',
+ 'dispatching': 'd',
+ 'signing': 'S',
+}
+
+
+# os.path.samefile is available only under Unix
+def os_path_samefile(path1, path2):
+ try:
+ return os.path.samefile(path1, path2)
+ except:
+ return os.path.realpath(path1) == os.path.realpath(path2)
+
+class File:
+ """represent a file, including its metadata"""
+ def __init__(self, name, md5, size, mtime, skipped=False):
+ self.name = name
+ self.md5 = md5
+ self.size = size
+ self.mtime = mtime
+ self.skipped = skipped
+ def __repr__(self):
+ return self.name
+ def __str__(self):
+ return self.name
+
+
+class Serviceinfo:
+ """Source service content
+ """
+ def __init__(self):
+ """creates an empty serviceinfo instance"""
+ self.services = []
+ self.project = None
+ self.package = None
+
+ def read(self, serviceinfo_node, append=False):
+ """read in the source services <services> element passed as
+ elementtree node.
+ """
+ def error(msg, xml):
+ data = 'invalid service format:\n%s' % ET.tostring(xml, encoding=ET_ENCODING)
+ raise ValueError("%s\n\n%s" % (data, msg))
+
+ if serviceinfo_node is None:
+ return
+ if not append:
+ self.services = []
+ services = serviceinfo_node.findall('service')
+
+ for service in services:
+ name = service.get('name')
+ if len(name) < 3 or '/' in name:
+ error("invalid service name: %s" % name, service)
+ mode = service.get('mode', '')
+ data = { 'name' : name, 'mode' : mode }
+ command = [ name ]
+ for param in service.findall('param'):
+ option = param.get('name')
+ if option is None:
+ error("%s: a parameter requires a name" % name, service)
+ value = ''
+ if param.text:
+ value = param.text
+ command.append('--' + option)
+ # hmm is this reasonable or do we want to allow real
+ # options (e.g., "--force" (without an argument)) as well?
+ command.append(value)
+ data['command'] = command
+ self.services.append(data)
+
+ def getProjectGlobalServices(self, apiurl, project, package):
+ # get all project wide services in one file, we don't store it yet
+ u = makeurl(apiurl, ['source', project, package], query='cmd=getprojectservices')
+ try:
+ f = http_POST(u)
+ root = ET.parse(f).getroot()
+ self.read(root, True)
+ self.project = project
+ self.package = package
+ except HTTPError as e:
+ if e.code != 403 and e.code != 400:
+ raise e
+
+ def addVerifyFile(self, serviceinfo_node, filename):
+ import hashlib
+
+ f = open(filename, 'r')
+ digest = hashlib.sha256(f.read()).hexdigest()
+ f.close()
+
+ r = serviceinfo_node
+ s = ET.Element( "service", name="verify_file" )
+ ET.SubElement(s, "param", name="file").text = filename
+ ET.SubElement(s, "param", name="verifier").text = "sha256"
+ ET.SubElement(s, "param", name="checksum").text = digest
+
+ r.append( s )
+ return r
+
+
+ def addDownloadUrl(self, serviceinfo_node, url_string):
+ url = urlparse( url_string )
+ protocol = url.scheme
+ host = url.netloc
+ path = url.path
+
+ r = serviceinfo_node
+ s = ET.Element( "service", name="download_url" )
+ ET.SubElement(s, "param", name="protocol").text = protocol
+ ET.SubElement(s, "param", name="host").text = host
+ ET.SubElement(s, "param", name="path").text = path
+
+ r.append( s )
+ return r
+
+ def addSetVersion(self, serviceinfo_node):
+ r = serviceinfo_node
+ s = ET.Element( "service", name="set_version", mode="buildtime" )
+ r.append( s )
+ return r
+
+ def addGitUrl(self, serviceinfo_node, url_string):
+ r = serviceinfo_node
+ s = ET.Element( "service", name="obs_scm" )
+ ET.SubElement(s, "param", name="url").text = url_string
+ ET.SubElement(s, "param", name="scm").text = "git"
+ r.append( s )
+ return r
+
+ def addTarUp(self, serviceinfo_node):
+ r = serviceinfo_node
+ s = ET.Element( "service", name="tar", mode="buildtime" )
+ r.append( s )
+ return r
+
+ def addRecompressTar(self, serviceinfo_node):
+ r = serviceinfo_node
+ s = ET.Element( "service", name="recompress", mode="buildtime" )
+ ET.SubElement(s, "param", name="file").text = "*.tar"
+ ET.SubElement(s, "param", name="compression").text = "xz"
+ r.append( s )
+ return r
+
+ def execute(self, dir, callmode = None, singleservice = None, verbose = None):
+ import tempfile
+
+ # cleanup existing generated files
+ for filename in os.listdir(dir):
+ if filename.startswith('_service:') or filename.startswith('_service_'):
+ ent = os.path.join(dir, filename)
+ if os.path.isdir(ent):
+ shutil.rmtree(ent)
+ else:
+ os.unlink(ent)
+
+ allservices = self.services or []
+ if singleservice and not singleservice in allservices:
+ # set array to the manual specified singleservice, if it is not part of _service file
+ data = { 'name' : singleservice, 'command' : [ singleservice ], 'mode' : '' }
+ allservices = [data]
+
+ # services can detect that they run via osc this way
+ os.putenv("OSC_VERSION", get_osc_version())
+
+ # set environment when using OBS 2.3 or later
+ if self.project != None:
+ os.putenv("OBS_SERVICE_PROJECT", self.project)
+ os.putenv("OBS_SERVICE_PACKAGE", self.package)
+
+ # recreate files
+ ret = 0
+ for service in allservices:
+ if singleservice and service['name'] != singleservice:
+ continue
+ if service['mode'] == "buildtime":
+ continue
+ if service['mode'] == "serveronly" and callmode != "disabled":
+ continue
+ if service['mode'] == "disabled" and callmode != "disabled":
+ continue
+ if service['mode'] != "disabled" and callmode == "disabled":
+ continue
+ if service['mode'] != "trylocal" and service['mode'] != "localonly" and callmode == "trylocal":
+ continue
+ temp_dir = None
+ try:
+ temp_dir = tempfile.mkdtemp(dir=dir, suffix='.%s.service' % service['name'])
+ cmd = service['command']
+ if not os.path.exists("/usr/lib/obs/service/"+cmd[0]):
+ raise oscerr.PackageNotInstalled("obs-service-%s"%cmd[0])
+ cmd[0] = "/usr/lib/obs/service/"+cmd[0]
+ cmd = cmd + [ "--outdir", temp_dir ]
+ if conf.config['verbose'] > 1 or verbose or conf.config['debug']:
+ print("Run source service:", ' '.join(cmd))
+ r = run_external(*cmd)
+
+ if r != 0:
+ print("Aborting: service call failed: ", ' '.join(cmd))
+ # FIXME: addDownloadUrlService calls si.execute after
+ # updating _services.
+ return r
+
+ if service['mode'] == "disabled" or service['mode'] == "trylocal" or service['mode'] == "localonly" or callmode == "local" or callmode == "trylocal":
+ for filename in os.listdir(temp_dir):
+ os.rename(os.path.join(temp_dir, filename), os.path.join(dir, filename))
+ else:
+ name = service['name']
+ for filename in os.listdir(temp_dir):
+ os.rename(os.path.join(temp_dir, filename), os.path.join(dir, "_service:"+name+":"+filename))
+ finally:
+ if temp_dir is not None:
+ shutil.rmtree(temp_dir)
+
+ return 0
+
+class Linkinfo:
+ """linkinfo metadata (which is part of the xml representing a directory
+ """
+ def __init__(self):
+ """creates an empty linkinfo instance"""
+ self.project = None
+ self.package = None
+ self.xsrcmd5 = None
+ self.lsrcmd5 = None
+ self.srcmd5 = None
+ self.error = None
+ self.rev = None
+ self.baserev = None
+
+ def read(self, linkinfo_node):
+ """read in the linkinfo metadata from the <linkinfo> element passed as
+ elementtree node.
+ If the passed element is None, the method does nothing.
+ """
+ if linkinfo_node == None:
+ return
+ self.project = linkinfo_node.get('project')
+ self.package = linkinfo_node.get('package')
+ self.xsrcmd5 = linkinfo_node.get('xsrcmd5')
+ self.lsrcmd5 = linkinfo_node.get('lsrcmd5')
+ self.srcmd5 = linkinfo_node.get('srcmd5')
+ self.error = linkinfo_node.get('error')
+ self.rev = linkinfo_node.get('rev')
+ self.baserev = linkinfo_node.get('baserev')
+
+ def islink(self):
+ """returns True if the linkinfo is not empty, otherwise False"""
+ if self.xsrcmd5 or self.lsrcmd5:
+ return True
+ return False
+
+ def isexpanded(self):
+ """returns True if the package is an expanded link"""
+ if self.lsrcmd5 and not self.xsrcmd5:
+ return True
+ return False
+
+ def haserror(self):
+ """returns True if the link is in error state (could not be applied)"""
+ if self.error:
+ return True
+ return False
+
+ def __str__(self):
+ """return an informatory string representation"""
+ if self.islink() and not self.isexpanded():
+ return 'project %s, package %s, xsrcmd5 %s, rev %s' \
+ % (self.project, self.package, self.xsrcmd5, self.rev)
+ elif self.islink() and self.isexpanded():
+ if self.haserror():
+ return 'broken link to project %s, package %s, srcmd5 %s, lsrcmd5 %s: %s' \
+ % (self.project, self.package, self.srcmd5, self.lsrcmd5, self.error)
+ else:
+ return 'expanded link to project %s, package %s, srcmd5 %s, lsrcmd5 %s' \
+ % (self.project, self.package, self.srcmd5, self.lsrcmd5)
+ else:
+ return 'None'
+
+class DirectoryServiceinfo:
+ def __init__(self):
+ self.code = None
+ self.xsrcmd5 = None
+ self.lsrcmd5 = None
+ self.error = ''
+
+ def read(self, serviceinfo_node):
+ if serviceinfo_node is None:
+ return
+ self.code = serviceinfo_node.get('code')
+ self.xsrcmd5 = serviceinfo_node.get('xsrcmd5')
+ self.lsrcmd5 = serviceinfo_node.get('lsrcmd5')
+ self.error = serviceinfo_node.find('error')
+ if self.error:
+ self.error = self.error.text
+
+ def isexpanded(self):
+ """
+ Returns true, if the directory contains the "expanded"/generated service files
+ """
+ return self.lsrcmd5 is not None and self.xsrcmd5 is None
+
+ def haserror(self):
+ return self.error is not None
+
+# http://effbot.org/zone/element-lib.htm#prettyprint
+def xmlindent(elem, level=0):
+ i = "\n" + level*" "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ for e in elem:
+ xmlindent(e, level+1)
+ if not e.tail or not e.tail.strip():
+ e.tail = i + " "
+ if not e.tail or not e.tail.strip():
+ e.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+class Project:
+ """
+ Represent a checked out project directory, holding packages.
+
+ :Attributes:
+ ``dir``
+ The directory path containing the project.
+
+ ``name``
+ The name of the project.
+
+ ``apiurl``
+ The endpoint URL of the API server.
+
+ ``pacs_available``
+ List of names of packages available server-side.
+ This is only populated if ``getPackageList`` is set
+ to ``True`` in the constructor.
+
+ ``pacs_have``
+ List of names of packages which exist server-side
+ and exist in the local project working copy (if
+ 'do_package_tracking' is disabled).
+ If 'do_package_tracking' is enabled it represents the
+ list names of packages which are tracked in the project
+ working copy (that is it might contain packages which
+ exist on the server as well as packages which do not
+ exist on the server (for instance if the local package
+ was added or if the package was removed on the server-side)).
+
+ ``pacs_excluded``
+ List of names of packages in the local project directory
+ which are excluded by the `exclude_glob` configuration
+ variable. Only set if `do_package_tracking` is enabled.
+
+ ``pacs_unvers``
+ List of names of packages in the local project directory
+ which are not tracked. Only set if `do_package_tracking`
+ is enabled.
+
+ ``pacs_broken``
+ List of names of packages which are tracked but do not
+ exist in the local project working copy. Only set if
+ `do_package_tracking` is enabled.
+
+ ``pacs_missing``
+ List of names of packages which exist server-side but
+ are not expected to exist in the local project directory.
+ """
+
+ REQ_STOREFILES = ('_project', '_apiurl')
+
+ def __init__(self, dir, getPackageList=True, progress_obj=None, wc_check=True):
+ """
+ Constructor.
+
+ :Parameters:
+ `dir` : str
+ The directory path containing the checked out project.
+
+ `getPackageList` : bool
+ Set to `False` if you want to skip retrieval from the
+ server of the list of packages in the project .
+
+ `wc_check` : bool
+ """
+ import fnmatch
+ self.dir = dir
+ self.absdir = os.path.abspath(dir)
+ self.progress_obj = progress_obj
+
+ self.name = store_read_project(self.dir)
+ self.apiurl = store_read_apiurl(self.dir, defaulturl=not wc_check)
+
+ dirty_files = []
+ if wc_check:
+ dirty_files = self.wc_check()
+ if dirty_files:
+ msg = 'Your working copy \'%s\' is in an inconsistent state.\n' \
+ 'Please run \'osc repairwc %s\' and check the state\n' \
+ 'of the working copy afterwards (via \'osc status %s\')' % (self.dir, self.dir, self.dir)
+ raise oscerr.WorkingCopyInconsistent(self.name, None, dirty_files, msg)
+
+ if getPackageList:
+ self.pacs_available = meta_get_packagelist(self.apiurl, self.name)
+ else:
+ self.pacs_available = []
+
+ if conf.config['do_package_tracking']:
+ self.pac_root = self.read_packages().getroot()
+ self.pacs_have = [ pac.get('name') for pac in self.pac_root.findall('package') ]
+ self.pacs_excluded = [ i for i in os.listdir(self.dir)
+ for j in conf.config['exclude_glob']
+ if fnmatch.fnmatch(i, j) ]
+ self.pacs_unvers = [ i for i in os.listdir(self.dir) if i not in self.pacs_have and i not in self.pacs_excluded ]
+ # store all broken packages (e.g. packages which where removed by a non-osc cmd)
+ # in the self.pacs_broken list
+ self.pacs_broken = []
+ for p in self.pacs_have:
+ if not os.path.isdir(os.path.join(self.absdir, p)):
+ # all states will be replaced with the '!'-state
+ # (except it is already marked as deleted ('D'-state))
+ self.pacs_broken.append(p)
+ else:
+ self.pacs_have = [ i for i in os.listdir(self.dir) if i in self.pacs_available ]
+
+ self.pacs_missing = [ i for i in self.pacs_available if i not in self.pacs_have ]
+
+ def wc_check(self):
+ global store
+ dirty_files = []
+ req_storefiles = Project.REQ_STOREFILES
+ if conf.config['do_package_tracking']:
+ req_storefiles += ('_packages',)
+ for fname in req_storefiles:
+ if not os.path.exists(os.path.join(self.absdir, store, fname)):
+ dirty_files.append(fname)
+ return dirty_files
+
+ def wc_repair(self, apiurl=None):
+ global store
+ if not os.path.exists(os.path.join(self.dir, store, '_apiurl')) or apiurl:
+ if apiurl is None:
+ msg = 'cannot repair wc: the \'_apiurl\' file is missing but ' \
+ 'no \'apiurl\' was passed to wc_repair'
+ # hmm should we raise oscerr.WrongArgs?
+ raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, [], msg)
+ # sanity check
+ conf.parse_apisrv_url(None, apiurl)
+ store_write_apiurl(self.dir, apiurl)
+ self.apiurl = store_read_apiurl(self.dir, defaulturl=False)
+
+ def checkout_missing_pacs(self, expand_link=False):
+ for pac in self.pacs_missing:
+
+ if conf.config['do_package_tracking'] and pac in self.pacs_unvers:
+ # pac is not under version control but a local file/dir exists
+ msg = 'can\'t add package \'%s\': Object already exists' % pac
+ raise oscerr.PackageExists(self.name, pac, msg)
+ else:
+ print('checking out new package %s' % pac)
+ checkout_package(self.apiurl, self.name, pac, \
+ pathname=getTransActPath(os.path.join(self.dir, pac)), \
+ prj_obj=self, prj_dir=self.dir, expand_link=expand_link, progress_obj=self.progress_obj)
+
+ def status(self, pac):
+ exists = os.path.exists(os.path.join(self.absdir, pac))
+ st = self.get_state(pac)
+ if st is None and exists:
+ return '?'
+ elif st is None:
+ raise oscerr.OscIOError(None, 'osc: \'%s\' is not under version control' % pac)
+ elif st in ('A', ' ') and not exists:
+ return '!'
+ elif st == 'D' and not exists:
+ return 'D'
+ else:
+ return st
+
+ def get_status(self, *exclude_states):
+ res = []
+ for pac in self.pacs_have:
+ st = self.status(pac)
+ if not st in exclude_states:
+ res.append((st, pac))
+ if not '?' in exclude_states:
+ res.extend([('?', pac) for pac in self.pacs_unvers])
+ return res
+
+ def get_pacobj(self, pac, *pac_args, **pac_kwargs):
+ try:
+ st = self.status(pac)
+ if st in ('?', '!') or st == 'D' and not os.path.exists(os.path.join(self.dir, pac)):
+ return None
+ return Package(os.path.join(self.dir, pac), *pac_args, **pac_kwargs)
+ except oscerr.OscIOError:
+ return None
+
+ def set_state(self, pac, state):
+ node = self.get_package_node(pac)
+ if node == None:
+ self.new_package_entry(pac, state)
+ else:
+ node.set('state', state)
+
+ def get_package_node(self, pac):
+ for node in self.pac_root.findall('package'):
+ if pac == node.get('name'):
+ return node
+ return None
+
+ def del_package_node(self, pac):
+ for node in self.pac_root.findall('package'):
+ if pac == node.get('name'):
+ self.pac_root.remove(node)
+
+ def get_state(self, pac):
+ node = self.get_package_node(pac)
+ if node != None:
+ return node.get('state')
+ else:
+ return None
+
+ def new_package_entry(self, name, state):
+ ET.SubElement(self.pac_root, 'package', name=name, state=state)
+
+ def read_packages(self):
+ """
+ Returns an ``xml.etree.cElementTree`` object representing the
+ parsed contents of the project's ``.osc/_packages`` XML file.
+ """
+ global store
+
+ packages_file = os.path.join(self.absdir, store, '_packages')
+ if os.path.isfile(packages_file) and os.path.getsize(packages_file):
+ try:
+ result = ET.parse(packages_file)
+ except:
+ msg = 'Cannot read package file \'%s\'. ' % packages_file
+ msg += 'You can try to remove it and then run osc repairwc.'
+ raise oscerr.OscIOError(None, msg)
+ return result
+ else:
+ # scan project for existing packages and migrate them
+ cur_pacs = []
+ for data in os.listdir(self.dir):
+ pac_dir = os.path.join(self.absdir, data)
+ # we cannot use self.pacs_available because we cannot guarantee that the package list
+ # was fetched from the server
+ if data in meta_get_packagelist(self.apiurl, self.name) and is_package_dir(pac_dir) \
+ and Package(pac_dir).name == data:
+ cur_pacs.append(ET.Element('package', name=data, state=' '))
+ store_write_initial_packages(self.absdir, self.name, cur_pacs)
+ return ET.parse(os.path.join(self.absdir, store, '_packages'))
+
+ def write_packages(self):
+ xmlindent(self.pac_root)
+ store_write_string(self.absdir, '_packages', ET.tostring(self.pac_root, encoding=ET_ENCODING))
+
+ def addPackage(self, pac):
+ import fnmatch
+ for i in conf.config['exclude_glob']:
+ if fnmatch.fnmatch(pac, i):
+ msg = 'invalid package name: \'%s\' (see \'exclude_glob\' config option)' % pac
+ raise oscerr.OscIOError(None, msg)
+ state = self.get_state(pac)
+ if state == None or state == 'D':
+ self.new_package_entry(pac, 'A')
+ self.write_packages()
+ # sometimes the new pac doesn't exist in the list because
+ # it would take too much time to update all data structs regularly
+ if pac in self.pacs_unvers:
+ self.pacs_unvers.remove(pac)
+ else:
+ raise oscerr.PackageExists(self.name, pac, 'package \'%s\' is already under version control' % pac)
+
+ def delPackage(self, pac, force = False):
+ state = self.get_state(pac.name)
+ can_delete = True
+ if state == ' ' or state == 'D':
+ del_files = []
+ for filename in pac.filenamelist + pac.filenamelist_unvers:
+ filestate = pac.status(filename)
+ if filestate == 'M' or filestate == 'C' or \
+ filestate == 'A' or filestate == '?':
+ can_delete = False
+ else:
+ del_files.append(filename)
+ if can_delete or force:
+ for filename in del_files:
+ pac.delete_localfile(filename)
+ if pac.status(filename) != '?':
+ # this is not really necessary
+ pac.put_on_deletelist(filename)
+ print(statfrmt('D', getTransActPath(os.path.join(pac.dir, filename))))
+ print(statfrmt('D', getTransActPath(os.path.join(pac.dir, os.pardir, pac.name))))
+ pac.write_deletelist()
+ self.set_state(pac.name, 'D')
+ self.write_packages()
+ else:
+ print('package \'%s\' has local modifications (see osc st for details)' % pac.name)
+ elif state == 'A':
+ if force:
+ delete_dir(pac.absdir)
+ self.del_package_node(pac.name)
+ self.write_packages()
+ print(statfrmt('D', pac.name))
+ else:
+ print('package \'%s\' has local modifications (see osc st for details)' % pac.name)
+ elif state == None:
+ print('package is not under version control')
+ else:
+ print('unsupported state')
+
+ def update(self, pacs = (), expand_link=False, unexpand_link=False, service_files=False):
+ if len(pacs):
+ for pac in pacs:
+ Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj).update()
+ else:
+ # we need to make sure that the _packages file will be written (even if an exception
+ # occurs)
+ try:
+ # update complete project
+ # packages which no longer exists upstream
+ upstream_del = [ pac for pac in self.pacs_have if not pac in self.pacs_available and self.get_state(pac) != 'A']
+ sinfo_pacs = [pac for pac in self.pacs_have if self.get_state(pac) in (' ', 'D') and not pac in self.pacs_broken]
+ sinfos = get_project_sourceinfo(self.apiurl, self.name, True, *sinfo_pacs)
+
+ for pac in upstream_del:
+ if self.status(pac) != '!':
+ p = Package(os.path.join(self.dir, pac))
+ self.delPackage(p, force = True)
+ delete_storedir(p.storedir)
+ try:
+ os.rmdir(pac)
+ except:
+ pass
+ self.pac_root.remove(self.get_package_node(pac))
+ self.pacs_have.remove(pac)
+
+ for pac in self.pacs_have:
+ state = self.get_state(pac)
+ if pac in self.pacs_broken:
+ if self.get_state(pac) != 'A':
+ checkout_package(self.apiurl, self.name, pac,
+ pathname=getTransActPath(os.path.join(self.dir, pac)), prj_obj=self,
+ prj_dir=self.dir, expand_link=not unexpand_link, progress_obj=self.progress_obj)
+ elif state == ' ':
+ # do a simple update
+ p = Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj)
+ rev = None
+ needs_update = True
+ if expand_link and p.islink() and not p.isexpanded():
+ if p.haslinkerror():
+ try:
+ rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev)
+ except:
+ rev = show_upstream_xsrcmd5(p.apiurl, p.prjname, p.name, revision=p.rev, linkrev="base")
+ p.mark_frozen()
+ else:
+ rev = p.linkinfo.xsrcmd5
+ print('Expanding to rev', rev)
+ elif unexpand_link and p.islink() and p.isexpanded():
+ rev = p.linkinfo.lsrcmd5
+ print('Unexpanding to rev', rev)
+ elif p.islink() and p.isexpanded():
+ needs_update = p.update_needed(sinfos[p.name])
+ if needs_update:
+ rev = p.latest_rev()
+ elif p.hasserviceinfo() and p.serviceinfo.isexpanded() and not service_files:
+ # FIXME: currently, do_update does not propagate the --server-side-source-service-files
+ # option to this method. Consequence: an expanded service is always unexpanded during
+ # an update (TODO: discuss if this is a reasonable behavior (at least this the default
+ # behavior for a while))
+ needs_update = True
+ else:
+ needs_update = p.update_needed(sinfos[p.name])
+ print('Updating %s' % p.name)
+ if needs_update:
+ p.update(rev, service_files)
+ else:
+ print('At revision %s.' % p.rev)
+ if unexpand_link:
+ p.unmark_frozen()
+ elif state == 'D':
+ # pac exists (the non-existent pac case was handled in the first if block)
+ p = Package(os.path.join(self.dir, pac), progress_obj=self.progress_obj)
+ if p.update_needed(sinfos[p.name]):
+ p.update()
+ elif state == 'A' and pac in self.pacs_available:
+ # file/dir called pac already exists and is under version control
+ msg = 'can\'t add package \'%s\': Object already exists' % pac
+ raise oscerr.PackageExists(self.name, pac, msg)
+ elif state == 'A':
+ # do nothing
+ pass
+ else:
+ print('unexpected state.. package \'%s\'' % pac)
+
+ self.checkout_missing_pacs(expand_link=not unexpand_link)
+ finally:
+ self.write_packages()
+
+ def commit(self, pacs = (), msg = '', files = {}, verbose = False, skip_local_service_run = False, can_branch=False, force=False):
+ if len(pacs):
+ try:
+ for pac in pacs:
+ todo = []
+ if pac in files:
+ todo = files[pac]
+ state = self.get_state(pac)
+ if state == 'A':
+ self.commitNewPackage(pac, msg, todo, verbose=verbose, skip_local_service_run=skip_local_service_run)
+ elif state == 'D':
+ self.commitDelPackage(pac)
+ elif state == ' ':
+ # display the correct dir when sending the changes
+ if os_path_samefile(os.path.join(self.dir, pac), os.getcwd()):
+ p = Package('.')
+ else:
+ p = Package(os.path.join(self.dir, pac))
+ p.todo = todo
+ p.commit(msg, verbose=verbose, skip_local_service_run=skip_local_service_run, can_branch=can_branch, force=force)
+ elif pac in self.pacs_unvers and not is_package_dir(os.path.join(self.dir, pac)):
+ print('osc: \'%s\' is not under version control' % pac)
+ elif pac in self.pacs_broken:
+ print('osc: \'%s\' package not found' % pac)
+ elif state == None:
+ self.commitExtPackage(pac, msg, todo, verbose=verbose, skip_local_service_run=skip_local_service_run)
+ finally:
+ self.write_packages()
+ else:
+ # if we have packages marked as '!' we cannot commit
+ for pac in self.pacs_broken:
+ if self.get_state(pac) != 'D':
+ msg = 'commit failed: package \'%s\' is missing' % pac
+ raise oscerr.PackageMissing(self.name, pac, msg)
+ try:
+ for pac in self.pacs_have:
+ state = self.get_state(pac)
+ if state == ' ':
+ # do a simple commit
+ Package(os.path.join(self.dir, pac)).commit(msg, verbose=verbose, skip_local_service_run=skip_local_service_run)
+ elif state == 'D':
+ self.commitDelPackage(pac)
+ elif state == 'A':
+ self.commitNewPackage(pac, msg, verbose=verbose, skip_local_service_run=skip_local_service_run)
+ finally:
+ self.write_packages()
+
+ def commitNewPackage(self, pac, msg = '', files = [], verbose = False, skip_local_service_run = False):
+ """creates and commits a new package if it does not exist on the server"""
+ if pac in self.pacs_available:
+ print('package \'%s\' already exists' % pac)
+ else:
+ user = conf.get_apiurl_usr(self.apiurl)
+ edit_meta(metatype='pkg',
+ path_args=(quote_plus(self.name), quote_plus(pac)),
+ template_args=({
+ 'name': pac,
+ 'user': user}),
+ apiurl=self.apiurl)
+ # display the correct dir when sending the changes
+ olddir = os.getcwd()
+ if os_path_samefile(os.path.join(self.dir, pac), os.curdir):
+ os.chdir(os.pardir)
+ p = Package(pac)
+ else:
+ p = Package(os.path.join(self.dir, pac))
+ p.todo = files
+ print(statfrmt('Sending', os.path.normpath(p.dir)))
+ p.commit(msg=msg, verbose=verbose, skip_local_service_run=skip_local_service_run)
+ self.set_state(pac, ' ')
+ os.chdir(olddir)
+
+ def commitDelPackage(self, pac):
+ """deletes a package on the server and in the working copy"""
+ try:
+ # display the correct dir when sending the changes
+ if os_path_samefile(os.path.join(self.dir, pac), os.curdir):
+ pac_dir = pac
+ else:
+ pac_dir = os.path.join(self.dir, pac)
+ p = Package(os.path.join(self.dir, pac))
+ #print statfrmt('Deleting', os.path.normpath(os.path.join(p.dir, os.pardir, pac)))
+ delete_storedir(p.storedir)
+ try:
+ os.rmdir(p.dir)
+ except:
+ pass
+ except OSError:
+ pac_dir = os.path.join(self.dir, pac)
+ #print statfrmt('Deleting', getTransActPath(os.path.join(self.dir, pac)))
+ print(statfrmt('Deleting', getTransActPath(pac_dir)))
+ delete_package(self.apiurl, self.name, pac)
+ self.del_package_node(pac)
+
+ def commitExtPackage(self, pac, msg, files = [], verbose=False, skip_local_service_run=False):
+ """commits a package from an external project"""
+ if os_path_samefile(os.path.join(self.dir, pac), os.getcwd()):
+ pac_path = '.'
+ else:
+ pac_path = os.path.join(self.dir, pac)
+
+ project = store_read_project(pac_path)
+ package = store_read_package(pac_path)
+ apiurl = store_read_apiurl(pac_path, defaulturl=False)
+ if not meta_exists(metatype='pkg',
+ path_args=(quote_plus(project), quote_plus(package)),
+ template_args=None, create_new=False, apiurl=apiurl):
+ user = conf.get_apiurl_usr(self.apiurl)
+ edit_meta(metatype='pkg',
+ path_args=(quote_plus(project), quote_plus(package)),
+ template_args=({'name': pac, 'user': user}), apiurl=apiurl)
+ p = Package(pac_path)
+ p.todo = files
+ p.commit(msg=msg, verbose=verbose, skip_local_service_run=skip_local_service_run)
+
+ def __str__(self):
+ r = []
+ r.append('*****************************************************')
+ r.append('Project %s (dir=%s, absdir=%s)' % (self.name, self.dir, self.absdir))
+ r.append('have pacs:\n%s' % ', '.join(self.pacs_have))
+ r.append('missing pacs:\n%s' % ', '.join(self.pacs_missing))
+ r.append('*****************************************************')
+ return '\n'.join(r)
+
+ @staticmethod
+ def init_project(apiurl, dir, project, package_tracking=True, getPackageList=True, progress_obj=None, wc_check=True):
+ global store
+
+ if not os.path.exists(dir):
+ # use makedirs (checkout_no_colon config option might be enabled)
+ os.makedirs(dir)
+ elif not os.path.isdir(dir):
+ raise oscerr.OscIOError(None, 'error: \'%s\' is no directory' % dir)
+ if os.path.exists(os.path.join(dir, store)):
+ raise oscerr.OscIOError(None, 'error: \'%s\' is already an initialized osc working copy' % dir)
+ else:
+ os.mkdir(os.path.join(dir, store))
+
+ store_write_project(dir, project)
+ store_write_apiurl(dir, apiurl)
+ if package_tracking:
+ store_write_initial_packages(dir, project, [])
+ return Project(dir, getPackageList, progress_obj, wc_check)
+
+
+class Package:
+ """represent a package (its directory) and read/keep/write its metadata"""
+
+ # should _meta be a required file?
+ REQ_STOREFILES = ('_project', '_package', '_apiurl', '_files', '_osclib_version')
+ OPT_STOREFILES = ('_to_be_added', '_to_be_deleted', '_in_conflict', '_in_update',
+ '_in_commit', '_meta', '_meta_mode', '_frozenlink', '_pulled', '_linkrepair',
+ '_size_limit', '_commit_msg')
+
+ def __init__(self, workingdir, progress_obj=None, size_limit=None, wc_check=True):
+ global store
+
+ self.dir = workingdir
+ self.absdir = os.path.abspath(self.dir)
+ self.storedir = os.path.join(self.absdir, store)
+ self.progress_obj = progress_obj
+ self.size_limit = size_limit
+ if size_limit and size_limit == 0:
+ self.size_limit = None
+
+ check_store_version(self.dir)
+
+ self.prjname = store_read_project(self.dir)
+ self.name = store_read_package(self.dir)
+ self.apiurl = store_read_apiurl(self.dir, defaulturl=not wc_check)
+
+ self.update_datastructs()
+ dirty_files = []
+ if wc_check:
+ dirty_files = self.wc_check()
+ if dirty_files:
+ msg = 'Your working copy \'%s\' is in an inconsistent state.\n' \
+ 'Please run \'osc repairwc %s\' (Note this might _remove_\n' \
+ 'files from the .osc/ dir). Please check the state\n' \
+ 'of the working copy afterwards (via \'osc status %s\')' % (self.dir, self.dir, self.dir)
+ raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, dirty_files, msg)
+
+ self.todo = []
+
+ def wc_check(self):
+ dirty_files = []
+ for fname in self.filenamelist:
+ if not os.path.exists(os.path.join(self.storedir, fname)) and not fname in self.skipped:
+ dirty_files.append(fname)
+ for fname in Package.REQ_STOREFILES:
+ if not os.path.isfile(os.path.join(self.storedir, fname)):
+ dirty_files.append(fname)
+ for fname in os.listdir(self.storedir):
+ if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \
+ fname.startswith('_build'):
+ continue
+ elif fname in self.filenamelist and fname in self.skipped:
+ dirty_files.append(fname)
+ elif not fname in self.filenamelist:
+ dirty_files.append(fname)
+ for fname in self.to_be_deleted[:]:
+ if not fname in self.filenamelist:
+ dirty_files.append(fname)
+ for fname in self.in_conflict[:]:
+ if not fname in self.filenamelist:
+ dirty_files.append(fname)
+ return dirty_files
+
+ def wc_repair(self, apiurl=None):
+ if not os.path.exists(os.path.join(self.storedir, '_apiurl')) or apiurl:
+ if apiurl is None:
+ msg = 'cannot repair wc: the \'_apiurl\' file is missing but ' \
+ 'no \'apiurl\' was passed to wc_repair'
+ # hmm should we raise oscerr.WrongArgs?
+ raise oscerr.WorkingCopyInconsistent(self.prjname, self.name, [], msg)
+ # sanity check
+ conf.parse_apisrv_url(None, apiurl)
+ store_write_apiurl(self.dir, apiurl)
+ self.apiurl = store_read_apiurl(self.dir, defaulturl=False)
+ # all files which are present in the filelist have to exist in the storedir
+ for f in self.filelist:
+ # XXX: should we also check the md5?
+ if not os.path.exists(os.path.join(self.storedir, f.name)) and not f.name in self.skipped:
+ # if get_source_file fails we're screwed up...
+ get_source_file(self.apiurl, self.prjname, self.name, f.name,
+ targetfilename=os.path.join(self.storedir, f.name), revision=self.rev,
+ mtime=f.mtime)
+ for fname in os.listdir(self.storedir):
+ if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \
+ fname.startswith('_build'):
+ continue
+ elif not fname in self.filenamelist or fname in self.skipped:
+ # this file does not belong to the storedir so remove it
+ os.unlink(os.path.join(self.storedir, fname))
+ for fname in self.to_be_deleted[:]:
+ if not fname in self.filenamelist:
+ self.to_be_deleted.remove(fname)
+ self.write_deletelist()
+ for fname in self.in_conflict[:]:
+ if not fname in self.filenamelist:
+ self.in_conflict.remove(fname)
+ self.write_conflictlist()
+
+ def info(self):
+ source_url = makeurl(self.apiurl, ['source', self.prjname, self.name])
+ r = info_templ % (self.prjname, self.name, self.absdir, self.apiurl, source_url, self.srcmd5, self.rev, self.linkinfo)
+ return r
+
+ def addfile(self, n):
+ if not os.path.exists(os.path.join(self.absdir, n)):
+ raise oscerr.OscIOError(None, 'error: file \'%s\' does not exist' % n)
+ if n in self.to_be_deleted:
+ self.to_be_deleted.remove(n)
+# self.delete_storefile(n)
+ self.write_deletelist()
+ elif n in self.filenamelist or n in self.to_be_added:
+ raise oscerr.PackageFileConflict(self.prjname, self.name, n, 'osc: warning: \'%s\' is already under version control' % n)
+# shutil.copyfile(os.path.join(self.dir, n), os.path.join(self.storedir, n))
+ if self.dir != '.':
+ pathname = os.path.join(self.dir, n)
+ else:
+ pathname = n
+ self.to_be_added.append(n)
+ self.write_addlist()
+ print(statfrmt('A', pathname))
+
+ def delete_file(self, n, force=False):
+ """deletes a file if possible and marks the file as deleted"""
+ state = '?'
+ try:
+ state = self.status(n)
+ except IOError as ioe:
+ if not force:
+ raise ioe
+ if state in ['?', 'A', 'M', 'R', 'C'] and not force:
+ return (False, state)
+ # special handling for skipped files: if file exists, simply delete it
+ if state == 'S':
+ exists = os.path.exists(os.path.join(self.dir, n))
+ self.delete_localfile(n)
+ return (exists, 'S')
+
+ self.delete_localfile(n)
+ was_added = n in self.to_be_added
+ if state in ('A', 'R') or state == '!' and was_added:
+ self.to_be_added.remove(n)
+ self.write_addlist()
+ elif state == 'C':
+ # don't remove "merge files" (*.r, *.mine...)
+ # that's why we don't use clear_from_conflictlist
+ self.in_conflict.remove(n)
+ self.write_conflictlist()
+ if not state in ('A', '?') and not (state == '!' and was_added):
+ self.put_on_deletelist(n)
+ self.write_deletelist()
+ return (True, state)
+
+ def delete_storefile(self, n):
+ try: os.unlink(os.path.join(self.storedir, n))
+ except: pass
+
+ def delete_localfile(self, n):
+ try: os.unlink(os.path.join(self.dir, n))
+ except: pass
+
+ def put_on_deletelist(self, n):
+ if n not in self.to_be_deleted:
+ self.to_be_deleted.append(n)
+
+ def put_on_conflictlist(self, n):
+ if n not in self.in_conflict:
+ self.in_conflict.append(n)
+
+ def put_on_addlist(self, n):
+ if n not in self.to_be_added:
+ self.to_be_added.append(n)
+
+ def clear_from_conflictlist(self, n):
+ """delete an entry from the file, and remove the file if it would be empty"""
+ if n in self.in_conflict:
+
+ filename = os.path.join(self.dir, n)
+ storefilename = os.path.join(self.storedir, n)
+ myfilename = os.path.join(self.dir, n + '.mine')
+ if self.islinkrepair() or self.ispulled():
+ upfilename = os.path.join(self.dir, n + '.new')
+ else:
+ upfilename = os.path.join(self.dir, n + '.r' + self.rev)
+
+ try:
+ os.unlink(myfilename)
+ # the working copy may be updated, so the .r* ending may be obsolete...
+ # then we don't care
+ os.unlink(upfilename)
+ if self.islinkrepair() or self.ispulled():
+ os.unlink(os.path.join(self.dir, n + '.old'))
+ except:
+ pass
+
+ self.in_conflict.remove(n)
+
+ self.write_conflictlist()
+
+ # XXX: this isn't used at all
+ def write_meta_mode(self):
+ # XXX: the "elif" is somehow a contradiction (with current and the old implementation
+ # it's not possible to "leave" the metamode again) (except if you modify pac.meta
+ # which is really ugly:) )
+ if self.meta:
+ store_write_string(self.absdir, '_meta_mode', '')
+ elif self.ismetamode():
+ os.unlink(os.path.join(self.storedir, '_meta_mode'))
+
+ def write_sizelimit(self):
+ if self.size_limit and self.size_limit <= 0:
+ try:
+ os.unlink(os.path.join(self.storedir, '_size_limit'))
+ except:
+ pass
+ else:
+ store_write_string(self.absdir, '_size_limit', str(self.size_limit) + '\n')
+
+ def write_addlist(self):
+ self.__write_storelist('_to_be_added', self.to_be_added)
+
+ def write_deletelist(self):
+ self.__write_storelist('_to_be_deleted', self.to_be_deleted)
+
+ def delete_source_file(self, n):
+ """delete local a source file"""
+ self.delete_localfile(n)
+ self.delete_storefile(n)
+
+ def delete_remote_source_file(self, n):
+ """delete a remote source file (e.g. from the server)"""
+ query = 'rev=upload'
+ u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
+ http_DELETE(u)
+
+ def put_source_file(self, n, tdir, copy_only=False):
+ query = 'rev=repository'
+ tfilename = os.path.join(tdir, n)
+ shutil.copyfile(os.path.join(self.dir, n), tfilename)
+ # escaping '+' in the URL path (note: not in the URL query string) is
+ # only a workaround for ruby on rails, which swallows it otherwise
+ if not copy_only:
+ u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
+ http_PUT(u, file = tfilename)
+ if n in self.to_be_added:
+ self.to_be_added.remove(n)
+
+ def __commit_update_store(self, tdir):
+ """move files from transaction directory into the store"""
+ for filename in os.listdir(tdir):
+ os.rename(os.path.join(tdir, filename), os.path.join(self.storedir, filename))
+
+ def __generate_commitlist(self, todo_send):
+ root = ET.Element('directory')
+ for i in sorted(todo_send.keys()):
+ ET.SubElement(root, 'entry', name=i, md5=todo_send[i])
+ return root
+
+ @staticmethod
+ def commit_filelist(apiurl, project, package, filelist, msg='', user=None, **query):
+ """send the commitlog and the local filelist to the server"""
+ if user is None:
+ user = conf.get_apiurl_usr(apiurl)
+ query.update({'cmd': 'commitfilelist', 'user': user, 'comment': msg})
+ u = makeurl(apiurl, ['source', project, package], query=query)
+ f = http_POST(u, data=ET.tostring(filelist, encoding=ET_ENCODING))
+ root = ET.parse(f).getroot()
+ return root
+
+ @staticmethod
+ def commit_get_missing(filelist):
+ """returns list of missing files (filelist is the result of commit_filelist)"""
+ error = filelist.get('error')
+ if error is None:
+ return []
+ elif error != 'missing':
+ raise oscerr.APIError('commit_get_missing_files: '
+ 'unexpected \'error\' attr: \'%s\'' % error)
+ todo = []
+ for n in filelist.findall('entry'):
+ name = n.get('name')
+ if name is None:
+ raise oscerr.APIError('missing \'name\' attribute:\n%s\n'
+ % ET.tostring(filelist, encoding=ET_ENCODING))
+ todo.append(n.get('name'))
+ return todo
+
+ def __send_commitlog(self, msg, local_filelist):
+ """send the commitlog and the local filelist to the server"""
+ query = {}
+ if self.islink() and self.isexpanded():
+ query['keeplink'] = '1'
+ if conf.config['linkcontrol'] or self.isfrozen():
+ query['linkrev'] = self.linkinfo.srcmd5
+ if self.ispulled():
+ query['repairlink'] = '1'
+ query['linkrev'] = self.get_pulled_srcmd5()
+ if self.islinkrepair():
+ query['repairlink'] = '1'
+ return self.commit_filelist(self.apiurl, self.prjname, self.name,
+ local_filelist, msg, **query)
+
+ def commit(self, msg='', verbose=False, skip_local_service_run=False, can_branch=False, force=False):
+ # commit only if the upstream revision is the same as the working copy's
+ upstream_rev = self.latest_rev()
+ if self.rev != upstream_rev:
+ raise oscerr.WorkingCopyOutdated((self.absdir, self.rev, upstream_rev))
+
+ if not skip_local_service_run:
+ r = self.run_source_services(mode="trylocal", verbose=verbose)
+ if r is not 0:
+ # FIXME: it is better to raise this in Serviceinfo.execute with more
+ # information (like which service/command failed)
+ raise oscerr.ServiceRuntimeError('A service failed with error: %d' % r)
+
+ # check if it is a link, if so, branch the package
+ if self.is_link_to_different_project():
+ if can_branch:
+ orgprj = self.get_local_origin_project()
+ print("Branching {} from {} to {}".format(self.name, orgprj, self.prjname))
+ exists, targetprj, targetpkg, srcprj, srcpkg = branch_pkg(
+ self.apiurl, orgprj, self.name, target_project=self.prjname)
+ # update _meta and _files to sychronize the local package
+ # to the new branched one in OBS
+ self.update_local_pacmeta()
+ self.update_local_filesmeta()
+ else:
+ print("{} Not commited because is link to a different project".format(self.name))
+ return 1
+
+ if not self.todo:
+ self.todo = [i for i in self.to_be_added if not i in self.filenamelist] + self.filenamelist
+
+ pathn = getTransActPath(self.dir)
+
+ todo_send = {}
+ todo_delete = []
+ real_send = []
+ for filename in self.filenamelist + [i for i in self.to_be_added if not i in self.filenamelist]:
+ if filename.startswith('_service:') or filename.startswith('_service_'):
+ continue
+ st = self.status(filename)
+ if st == 'C':
+ print('Please resolve all conflicts before committing using "osc resolved FILE"!')
+ return 1
+ elif filename in self.todo:
+ if st in ('A', 'R', 'M'):
+ todo_send[filename] = dgst(os.path.join(self.absdir, filename))
+ real_send.append(filename)
+ print(statfrmt('Sending', os.path.join(pathn, filename)))
+ elif st in (' ', '!', 'S'):
+ if st == '!' and filename in self.to_be_added:
+ print('file \'%s\' is marked as \'A\' but does not exist' % filename)
+ return 1
+ f = self.findfilebyname(filename)
+ if f is None:
+ raise oscerr.PackageInternalError(self.prjname, self.name,
+ 'error: file \'%s\' with state \'%s\' is not known by meta' \
+ % (filename, st))
+ todo_send[filename] = f.md5
+ elif st == 'D':
+ todo_delete.append(filename)
+ print(statfrmt('Deleting', os.path.join(pathn, filename)))
+ elif st in ('R', 'M', 'D', ' ', '!', 'S'):
+ # ignore missing new file (it's not part of the current commit)
+ if st == '!' and filename in self.to_be_added:
+ continue
+ f = self.findfilebyname(filename)
+ if f is None:
+ raise oscerr.PackageInternalError(self.prjname, self.name,
+ 'error: file \'%s\' with state \'%s\' is not known by meta' \
+ % (filename, st))
+ todo_send[filename] = f.md5
+
+ if not force and not real_send and not todo_delete and not self.islinkrepair() and not self.ispulled():
+ print('nothing to do for package %s' % self.name)
+ return 1
+
+ print('Transmitting file data', end=' ')
+ filelist = self.__generate_commitlist(todo_send)
+ sfilelist = self.__send_commitlog(msg, filelist)
+ send = self.commit_get_missing(sfilelist)
+ real_send = [i for i in real_send if not i in send]
+ # abort after 3 tries
+ tries = 3
+ tdir = None
+ try:
+ tdir = os.path.join(self.storedir, '_in_commit')
+ if os.path.isdir(tdir):
+ shutil.rmtree(tdir)
+ os.mkdir(tdir)
+ while len(send) and tries:
+ for filename in send[:]:
+ sys.stdout.write('.')
+ sys.stdout.flush()
+ self.put_source_file(filename, tdir)
+ send.remove(filename)
+ tries -= 1
+ sfilelist = self.__send_commitlog(msg, filelist)
+ send = self.commit_get_missing(sfilelist)
+ if len(send):
+ raise oscerr.PackageInternalError(self.prjname, self.name,
+ 'server does not accept filelist:\n%s\nmissing:\n%s\n' \
+ % (ET.tostring(filelist, encoding=ET_ENCODING), ET.tostring(sfilelist, encoding=ET_ENCODING)))
+ # these files already exist on the server
+ for filename in real_send:
+ self.put_source_file(filename, tdir, copy_only=True)
+ # update store with the committed files
+ self.__commit_update_store(tdir)
+ finally:
+ if tdir is not None and os.path.isdir(tdir):
+ shutil.rmtree(tdir)
+ self.rev = sfilelist.get('rev')
+ print()
+ print('Committed revision %s.' % self.rev)
+
+ if self.ispulled():
+ os.unlink(os.path.join(self.storedir, '_pulled'))
+ if self.islinkrepair():
+ os.unlink(os.path.join(self.storedir, '_linkrepair'))
+ self.linkrepair = False
+ # XXX: mark package as invalid?
+ print('The source link has been repaired. This directory can now be removed.')
+
+ if self.islink() and self.isexpanded():
+ li = Linkinfo()
+ li.read(sfilelist.find('linkinfo'))
+ if li.xsrcmd5 is None:
+ raise oscerr.APIError('linkinfo has no xsrcmd5 attr:\n%s\n' % ET.tostring(sfilelist, encoding=ET_ENCODING))
+ sfilelist = ET.fromstring(self.get_files_meta(revision=li.xsrcmd5))
+ for i in sfilelist.findall('entry'):
+ if i.get('name') in self.skipped:
+ i.set('skipped', 'true')
+ store_write_string(self.absdir, '_files', ET.tostring(sfilelist, encoding=ET_ENCODING) + '\n')
+ for filename in todo_delete:
+ self.to_be_deleted.remove(filename)
+ self.delete_storefile(filename)
+ self.write_deletelist()
+ self.write_addlist()
+ self.update_datastructs()
+
+ print_request_list(self.apiurl, self.prjname, self.name)
+
+ # FIXME: add testcases for this codepath
+ sinfo = sfilelist.find('serviceinfo')
+ if sinfo is not None:
+ print('Waiting for server side source service run')
+ u = makeurl(self.apiurl, ['source', self.prjname, self.name])
+ while sinfo is not None and sinfo.get('code') == 'running':
+ sys.stdout.write('.')
+ sys.stdout.flush()
+ # does it make sense to add some delay?
+ sfilelist = ET.fromstring(http_GET(u).read())
+ # if sinfo is None another commit might have occured in the "meantime"
+ sinfo = sfilelist.find('serviceinfo')
+ print('')
+ rev = self.latest_rev()
+ self.update(rev=rev)
+ elif self.get_local_meta() is None:
+ # if this was a newly added package there is no _meta
+ # file
+ self.update_local_pacmeta()
+
+ def __write_storelist(self, name, data):
+ if len(data) == 0:
+ try:
+ os.unlink(os.path.join(self.storedir, name))
+ except:
+ pass
+ else:
+ store_write_string(self.absdir, name, '%s\n' % '\n'.join(data))
+
+ def write_conflictlist(self):
+ self.__write_storelist('_in_conflict', self.in_conflict)
+
+ def updatefile(self, n, revision, mtime=None):
+ filename = os.path.join(self.dir, n)
+ storefilename = os.path.join(self.storedir, n)
+ origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
+ origfile = os.path.join(self.storedir, '_in_update', n)
+ if os.path.isfile(filename):
+ shutil.copyfile(filename, origfile_tmp)
+ os.rename(origfile_tmp, origfile)
+ else:
+ origfile = None
+
+ get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=storefilename,
+ revision=revision, progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
+
+ shutil.copyfile(storefilename, filename)
+ if mtime:
+ utime(filename, (-1, mtime))
+ if not origfile is None:
+ os.unlink(origfile)
+
+ def mergefile(self, n, revision, mtime=None):
+ filename = os.path.join(self.dir, n)
+ storefilename = os.path.join(self.storedir, n)
+ myfilename = os.path.join(self.dir, n + '.mine')
+ upfilename = os.path.join(self.dir, n + '.r' + self.rev)
+ origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n)
+ origfile = os.path.join(self.storedir, '_in_update', n)
+ shutil.copyfile(filename, origfile_tmp)
+ os.rename(origfile_tmp, origfile)
+ os.rename(filename, myfilename)
+
+ get_source_file(self.apiurl, self.prjname, self.name, n,
+ revision=revision, targetfilename=upfilename,
+ progress_obj=self.progress_obj, mtime=mtime, meta=self.meta)
+
+ if binary_file(myfilename) or binary_file(upfilename):
+ # don't try merging
+ shutil.copyfile(upfilename, filename)
+ shutil.copyfile(upfilename, storefilename)
+ os.unlink(origfile)
+ self.in_conflict.append(n)
+ self.write_conflictlist()
+ return 'C'
+ else:
+ # try merging
+ # diff3 OPTIONS... MINE OLDER YOURS
+ merge_cmd = 'diff3 -m -E %s %s %s > %s' % (myfilename, storefilename, upfilename, filename)
+ ret = run_external(merge_cmd, shell=True)
+
+ # "An exit status of 0 means `diff3' was successful, 1 means some
+ # conflicts were found, and 2 means trouble."
+ if ret == 0:
+ # merge was successful... clean up
+ shutil.copyfile(upfilename, storefilename)
+ os.unlink(upfilename)
+ os.unlink(myfilename)
+ os.unlink(origfile)
+ return 'G'
+ elif ret == 1:
+ # unsuccessful merge
+ shutil.copyfile(upfilename, storefilename)
+ os.unlink(origfile)
+ self.in_conflict.append(n)
+ self.write_conflictlist()
+ return 'C'
+ else:
+ raise oscerr.ExtRuntimeError('diff3 failed with exit code: %s' % ret, merge_cmd)
+
+ def update_local_filesmeta(self, revision=None):
+ """
+ Update the local _files file in the store.
+ It is replaced with the version pulled from upstream.
+ """
+ meta = self.get_files_meta(revision=revision)
+ store_write_string(self.absdir, '_files', meta + '\n')
+
+ def get_files_meta(self, revision='latest', skip_service=True):
+ fm = show_files_meta(self.apiurl, self.prjname, self.name, revision=revision, meta=self.meta)
+ # look for "too large" files according to size limit and mark them
+ root = ET.fromstring(fm)
+ for e in root.findall('entry'):
+ size = e.get('size')
+ if size and self.size_limit and int(size) > self.size_limit \
+ or skip_service and (e.get('name').startswith('_service:') or e.get('name').startswith('_service_')):
+ e.set('skipped', 'true')
+ return ET.tostring(root, encoding=ET_ENCODING)
+
+ def get_local_meta(self):
+ """Get the local _meta file for the package."""
+ meta = store_read_file(self.absdir, '_meta')
+ return meta
+
+ def get_local_origin_project(self):
+ """Get the originproject from the _meta file."""
+ # if the wc was checked out via some old osc version
+ # there might be no meta file: in this case we assume
+ # that the origin project is equal to the wc's project
+ meta = self.get_local_meta()
+ if meta is None:
+ return self.prjname
+ root = ET.fromstring(meta)
+ return root.get('project')
+
+ def is_link_to_different_project(self):
+ """Check if the package is a link to a different project."""
+ orgprj = self.get_local_origin_project()
+ return self.prjname != orgprj
+
+ def update_datastructs(self):
+ """
+ Update the internal data structures if the local _files
+ file has changed (e.g. update_local_filesmeta() has been
+ called).
+ """
+ import fnmatch
+ files_tree = read_filemeta(self.dir)
+ files_tree_root = files_tree.getroot()
+
+ self.rev = files_tree_root.get('rev')
+ self.srcmd5 = files_tree_root.get('srcmd5')
+
+ self.linkinfo = Linkinfo()
+ self.linkinfo.read(files_tree_root.find('linkinfo'))
+ self.serviceinfo = DirectoryServiceinfo()
+ self.serviceinfo.read(files_tree_root.find('serviceinfo'))
+
+ self.filenamelist = []
+ self.filelist = []
+ self.skipped = []
+ for node in files_tree_root.findall('entry'):
+ try:
+ f = File(node.get('name'),
+ node.get('md5'),
+ int(node.get('size')),
+ int(node.get('mtime')))
+ if node.get('skipped'):
+ self.skipped.append(f.name)
+ f.skipped = True
+ except:
+ # okay, a very old version of _files, which didn't contain any metadata yet...
+ f = File(node.get('name'), '', 0, 0)
+ self.filelist.append(f)
+ self.filenamelist.append(f.name)
+
+ self.to_be_added = read_tobeadded(self.absdir)
+ self.to_be_deleted = read_tobedeleted(self.absdir)
+ self.in_conflict = read_inconflict(self.absdir)
+ self.linkrepair = os.path.isfile(os.path.join(self.storedir, '_linkrepair'))
+ self.size_limit = read_sizelimit(self.dir)
+ self.meta = self.ismetamode()
+
+ # gather unversioned files, but ignore some stuff
+ self.excluded = []
+ for i in os.listdir(self.dir):
+ for j in conf.config['exclude_glob']:
+ if fnmatch.fnmatch(i, j):
+ self.excluded.append(i)
+ break
+ self.filenamelist_unvers = [ i for i in os.listdir(self.dir)
+ if i not in self.excluded
+ if i not in self.filenamelist ]
+
+ def islink(self):
+ """tells us if the package is a link (has 'linkinfo').
+ A package with linkinfo is a package which links to another package.
+ Returns True if the package is a link, otherwise False."""
+ return self.linkinfo.islink()
+
+ def isexpanded(self):
+ """tells us if the package is a link which is expanded.
+ Returns True if the package is expanded, otherwise False."""
+ return self.linkinfo.isexpanded()
+
+ def islinkrepair(self):
+ """tells us if we are repairing a broken source link."""
+ return self.linkrepair
+
+ def ispulled(self):
+ """tells us if we have pulled a link."""
+ return os.path.isfile(os.path.join(self.storedir, '_pulled'))
+
+ def isfrozen(self):
+ """tells us if the link is frozen."""
+ return os.path.isfile(os.path.join(self.storedir, '_frozenlink'))
+
+ def ismetamode(self):
+ """tells us if the package is in meta mode"""
+ return os.path.isfile(os.path.join(self.storedir, '_meta_mode'))
+
+ def get_pulled_srcmd5(self):
+ pulledrev = None
+ for line in open(os.path.join(self.storedir, '_pulled'), 'r'):
+ pulledrev = line.strip()
+ return pulledrev
+
+ def haslinkerror(self):
+ """
+ Returns True if the link is broken otherwise False.
+ If the package is not a link it returns False.
+ """
+ return self.linkinfo.haserror()
+
+ def linkerror(self):
+ """
+ Returns an error message if the link is broken otherwise None.
+ If the package is not a link it returns None.
+ """
+ return self.linkinfo.error
+
+ def hasserviceinfo(self):
+ """
+ Returns True, if this package contains services.
+ """
+ return self.serviceinfo.lsrcmd5 is not None or self.serviceinfo.xsrcmd5 is not None
+
+ def update_local_pacmeta(self):
+ """
+ Update the local _meta file in the store.
+ It is replaced with the version pulled from upstream.
+ """
+ meta = show_package_meta(self.apiurl, self.prjname, self.name)
+ if meta != "":
+ # is empty for _project for example
+ meta = ''.join(meta)
+ store_write_string(self.absdir, '_meta', meta + '\n')
+
+ def findfilebyname(self, n):
+ for i in self.filelist:
+ if i.name == n:
+ return i
+
+ def get_status(self, excluded=False, *exclude_states):
+ global store
+ todo = self.todo
+ if not todo:
+ todo = self.filenamelist + self.to_be_added + \
+ [i for i in self.filenamelist_unvers if not os.path.isdir(os.path.join(self.absdir, i))]
+ if excluded:
+ todo.extend([i for i in self.excluded if i != store])
+ todo = set(todo)
+ res = []
+ for fname in sorted(todo):
+ st = self.status(fname)
+ if not st in exclude_states:
+ res.append((st, fname))
+ return res
+
+ def status(self, n):
+ """
+ status can be:
+
+ file storefile file present STATUS
+ exists exists in _files
+
+ x - - 'A' and listed in _to_be_added
+ x x - 'R' and listed in _to_be_added
+ x x x ' ' if digest differs: 'M'
+ and if in conflicts file: 'C'
+ x - - '?'
+ - x x 'D' and listed in _to_be_deleted
+ x x x 'D' and listed in _to_be_deleted (e.g. if deleted file was modified)
+ x x x 'C' and listed in _in_conflict
+ x - x 'S' and listed in self.skipped
+ - - x 'S' and listed in self.skipped
+ - x x '!'
+ - - - NOT DEFINED
+
+ """
+
+ known_by_meta = False
+ exists = False
+ exists_in_store = False
+ if n in self.filenamelist:
+ known_by_meta = True
+ if os.path.exists(os.path.join(self.absdir, n)):
+ exists = True
+ if os.path.exists(os.path.join(self.storedir, n)):
+ exists_in_store = True
+
+ if n in self.to_be_deleted:
+ state = 'D'
+ elif n in self.in_conflict:
+ state = 'C'
+ elif n in self.skipped:
+ state = 'S'
+ elif n in self.to_be_added and exists and exists_in_store:
+ state = 'R'
+ elif n in self.to_be_added and exists:
+ state = 'A'
+ elif exists and exists_in_store and known_by_meta:
+ if dgst(os.path.join(self.absdir, n)) != self.findfilebyname(n).md5:
+ state = 'M'
+ else:
+ state = ' '
+ elif n in self.to_be_added and not exists:
+ state = '!'
+ elif not exists and exists_in_store and known_by_meta and not n in self.to_be_deleted:
+ state = '!'
+ elif exists and not exists_in_store and not known_by_meta:
+ state = '?'
+ elif not exists_in_store and known_by_meta:
+ # XXX: this codepath shouldn't be reached (we restore the storefile
+ # in update_datastructs)
+ raise oscerr.PackageInternalError(self.prjname, self.name,
+ 'error: file \'%s\' is known by meta but no storefile exists.\n'
+ 'This might be caused by an old wc format. Please backup your current\n'
+ 'wc and checkout the package again. Afterwards copy all files (except the\n'
+ '.osc/ dir) into the new package wc.' % n)
+ elif os.path.islink(os.path.join(self.absdir, n)):
+ # dangling symlink, whose name is _not_ tracked: treat it
+ # as unversioned
+ state = '?'
+ else:
+ # this case shouldn't happen (except there was a typo in the filename etc.)
+ raise oscerr.OscIOError(None, 'osc: \'%s\' is not under version control' % n)
+
+ return state
+
+ def get_diff(self, revision=None, ignoreUnversioned=False):
+ import tempfile
+ diff_hdr = 'Index: %s\n'
+ diff_hdr += '===================================================================\n'
+ kept = []
+ added = []
+ deleted = []
+ def diff_add_delete(fname, add, revision):
+ diff = []
+ diff.append(diff_hdr % fname)
+ tmpfile = None
+ origname = fname
+ if add:
+ diff.append('--- %s\t(revision 0)\n' % fname)
+ rev = 'revision 0'
+ if revision and not fname in self.to_be_added:
+ rev = 'working copy'
+ diff.append('+++ %s\t(%s)\n' % (fname, rev))
+ fname = os.path.join(self.absdir, fname)
+ else:
+ diff.append('--- %s\t(revision %s)\n' % (fname, revision or self.rev))
+ diff.append('+++ %s\t(working copy)\n' % fname)
+ fname = os.path.join(self.storedir, fname)
+
+ try:
+ if revision is not None and not add:
+ (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
+ get_source_file(self.apiurl, self.prjname, self.name, origname, tmpfile, revision)
+ fname = tmpfile
+ if binary_file(fname):
+ what = 'added'
+ if not add:
+ what = 'deleted'
+ diff = diff[:1]
+ diff.append('Binary file \'%s\' %s.\n' % (origname, what))
+ return diff
+ tmpl = '+%s'
+ ltmpl = '@@ -0,0 +1,%d @@\n'
+ if not add:
+ tmpl = '-%s'
+ ltmpl = '@@ -1,%d +0,0 @@\n'
+ lines = [tmpl % i for i in open(fname, 'r').readlines()]
+ if len(lines):
+ diff.append(ltmpl % len(lines))
+ if not lines[-1].endswith('\n'):
+ lines.append('\n\\ No newline at end of file\n')
+ diff.extend(lines)
+ finally:
+ if tmpfile is not None:
+ os.close(fd)
+ os.unlink(tmpfile)
+ return diff
+
+ if revision is None:
+ todo = self.todo or [i for i in self.filenamelist if not i in self.to_be_added]+self.to_be_added
+ for fname in todo:
+ if fname in self.to_be_added and self.status(fname) == 'A':
+ added.append(fname)
+ elif fname in self.to_be_deleted:
+ deleted.append(fname)
+ elif fname in self.filenamelist:
+ kept.append(self.findfilebyname(fname))
+ elif fname in self.to_be_added and self.status(fname) == '!':
+ raise oscerr.OscIOError(None, 'file \'%s\' is marked as \'A\' but does not exist\n'\
+ '(either add the missing file or revert it)' % fname)
+ elif not ignoreUnversioned:
+ raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % fname)
+ else:
+ fm = self.get_files_meta(revision=revision)
+ root = ET.fromstring(fm)
+ rfiles = self.__get_files(root)
+ # swap added and deleted
+ kept, deleted, added, services = self.__get_rev_changes(rfiles)
+ added = [f.name for f in added]
+ added.extend([f for f in self.to_be_added if not f in kept])
+ deleted = [f.name for f in deleted]
+ deleted.extend(self.to_be_deleted)
+ for f in added[:]:
+ if f in deleted:
+ added.remove(f)
+ deleted.remove(f)
+# print kept, added, deleted
+ for f in kept:
+ state = self.status(f.name)
+ if state in ('S', '?', '!'):
+ continue
+ elif state == ' ' and revision is None:
+ continue
+ elif revision and self.findfilebyname(f.name).md5 == f.md5 and state != 'M':
+ continue
+ yield [diff_hdr % f.name]
+ if revision is None:
+ yield get_source_file_diff(self.absdir, f.name, self.rev)
+ else:
+ tmpfile = None
+ diff = []
+ try:
+ (fd, tmpfile) = tempfile.mkstemp(prefix='osc_diff')
+ get_source_file(self.apiurl, self.prjname, self.name, f.name, tmpfile, revision)
+ diff = get_source_file_diff(self.absdir, f.name, revision,
+ os.path.basename(tmpfile), os.path.dirname(tmpfile), f.name)
+ finally:
+ if tmpfile is not None:
+ os.close(fd)
+ os.unlink(tmpfile)
+ yield diff
+
+ for f in added:
+ yield diff_add_delete(f, True, revision)
+ for f in deleted:
+ yield diff_add_delete(f, False, revision)
+
+ def merge(self, otherpac):
+ self.todo += otherpac.todo
+
+ def __str__(self):
+ r = """
+name: %s
+prjname: %s
+workingdir: %s
+localfilelist: %s
+linkinfo: %s
+rev: %s
+'todo' files: %s
+""" % (self.name,
+ self.prjname,
+ self.dir,
+ '\n '.join(self.filenamelist),
+ self.linkinfo,
+ self.rev,
+ self.todo)
+
+ return r
+
+
+ def read_meta_from_spec(self, spec = None):
+ import glob
+ if spec:
+ specfile = spec
+ else:
+ # scan for spec files
+ speclist = glob.glob(os.path.join(self.dir, '*.spec'))
+ if len(speclist) == 1:
+ specfile = speclist[0]
+ elif len(speclist) > 1:
+ print('the following specfiles were found:')
+ for filename in speclist:
+ print(filename)
+ print('please specify one with --specfile')
+ sys.exit(1)
+ else:
+ print('no specfile was found - please specify one ' \
+ 'with --specfile')
+ sys.exit(1)
+
+ data = read_meta_from_spec(specfile, 'Summary', 'Url', '%description')
+ self.summary = data.get('Summary', '')
+ self.url = data.get('Url', '')
+ self.descr = data.get('%description', '')
+
+
+ def update_package_meta(self, force=False):
+ """
+ for the updatepacmetafromspec subcommand
+ argument force supress the confirm question
+ """
+
+ m = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
+
+ root = ET.fromstring(m)
+ root.find('title').text = self.summary
+ root.find('description').text = ''.join(self.descr)
+ url = root.find('url')
+ if url == None:
+ url = ET.SubElement(root, 'url')
+ url.text = self.url
+
+ u = makeurl(self.apiurl, ['source', self.prjname, self.name, '_meta'])
+ mf = metafile(u, ET.tostring(root, encoding=ET_ENCODING))
+
+ if not force:
+ print('*' * 36, 'old', '*' * 36)
+ print(m)
+ print('*' * 36, 'new', '*' * 36)
+ print(ET.tostring(root, encoding=ET_ENCODING))
+ print('*' * 72)
+ repl = raw_input('Write? (y/N/e) ')
+ else:
+ repl = 'y'
+
+ if repl == 'y':
+ mf.sync()
+ elif repl == 'e':
+ mf.edit()
+
+ mf.discard()
+
+ def mark_frozen(self):
+ store_write_string(self.absdir, '_frozenlink', '')
+ print()
+ print("The link in this package (\"%s\") is currently broken. Checking" % self.name)
+ print("out the last working version instead; please use 'osc pull'")
+ print("to merge the conflicts.")
+ print()
+
+ def unmark_frozen(self):
+ if os.path.exists(os.path.join(self.storedir, '_frozenlink')):
+ os.unlink(os.path.join(self.storedir, '_frozenlink'))
+
+ def latest_rev(self, include_service_files=False, expand=False):
+ # if expand is True the xsrcmd5 will be returned (even if the wc is unexpanded)
+ if self.islinkrepair():
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrepair=1, meta=self.meta, include_service_files=include_service_files)
+ elif self.islink() and (self.isexpanded() or expand):
+ if self.isfrozen() or self.ispulled():
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
+ else:
+ try:
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
+ except:
+ try:
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev=self.linkinfo.srcmd5, meta=self.meta, include_service_files=include_service_files)
+ except:
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, linkrev="base", meta=self.meta, include_service_files=include_service_files)
+ self.mark_frozen()
+ elif not self.islink() and expand:
+ upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
+ else:
+ upstream_rev = show_upstream_rev(self.apiurl, self.prjname, self.name, meta=self.meta, include_service_files=include_service_files)
+ return upstream_rev
+
+ def __get_files(self, fmeta_root):
+ f = []
+ if fmeta_root.get('rev') is None and len(fmeta_root.findall('entry')) > 0:
+ raise oscerr.APIError('missing rev attribute in _files:\n%s' % ''.join(ET.tostring(fmeta_root, encoding=ET_ENCODING)))
+ for i in fmeta_root.findall('entry'):
+ error = i.get('error')
+ if error is not None:
+ raise oscerr.APIError('broken files meta: %s' % error)
+ skipped = i.get('skipped') is not None
+ f.append(File(i.get('name'), i.get('md5'),
+ int(i.get('size')), int(i.get('mtime')), skipped))
+ return f
+
+ def __get_rev_changes(self, revfiles):
+ kept = []
+ added = []
+ deleted = []
+ services = []
+ revfilenames = []
+ for f in revfiles:
+ revfilenames.append(f.name)
+ # treat skipped like deleted files
+ if f.skipped:
+ if f.name.startswith('_service:'):
+ services.append(f)
+ else:
+ deleted.append(f)
+ continue
+ # treat skipped like added files
+ # problem: this overwrites existing files during the update
+ # (because skipped files aren't in self.filenamelist_unvers)
+ if f.name in self.filenamelist and not f.name in self.skipped:
+ kept.append(f)
+ else:
+ added.append(f)
+ for f in self.filelist:
+ if not f.name in revfilenames:
+ deleted.append(f)
+
+ return kept, added, deleted, services
+
+ def update_needed(self, sinfo):
+ # this method might return a false-positive (that is a True is returned,
+ # even though no update is needed) (for details, see comments below)
+ if self.islink():
+ if self.isexpanded():
+ # check if both revs point to the same expanded sources
+ # Note: if the package contains a _service file, sinfo.srcmd5's lsrcmd5
+ # points to the "expanded" services (xservicemd5) => chances
+ # for a false-positive are high, because osc usually works on the
+ # "unexpanded" services.
+ # Once the srcserver supports something like noservice=1, we can get rid of
+ # this false-positives (patch was already sent to the ml) (but this also
+ # requires some slight changes in osc)
+ return sinfo.get('srcmd5') != self.srcmd5
+ elif self.hasserviceinfo():
+ # check if we have expanded or unexpanded services
+ if self.serviceinfo.isexpanded():
+ return sinfo.get('lsrcmd5') != self.srcmd5
+ else:
+ # again, we might have a false-positive here, because
+ # a mismatch of the "xservicemd5"s does not neccessarily
+ # imply a change in the "unexpanded" services.
+ return sinfo.get('lsrcmd5') != self.serviceinfo.xsrcmd5
+ # simple case: unexpanded sources and no services
+ # self.srcmd5 should also work
+ return sinfo.get('lsrcmd5') != self.linkinfo.lsrcmd5
+ elif self.hasserviceinfo():
+ if self.serviceinfo.isexpanded():
+ return sinfo.get('srcmd5') != self.srcmd5
+ else:
+ # cannot handle this case, because the sourceinfo does not contain
+ # information about the lservicemd5. Once the srcserver supports
+ # a noservice=1 query parameter, we can handle this case.
+ return True
+ return sinfo.get('srcmd5') != self.srcmd5
+
+ def update(self, rev = None, service_files = False, size_limit = None):
+ import tempfile
+ rfiles = []
+ # size_limit is only temporary for this update
+ old_size_limit = self.size_limit
+ if not size_limit is None:
+ self.size_limit = int(size_limit)
+ if os.path.isfile(os.path.join(self.storedir, '_in_update', '_files')):
+ print('resuming broken update...')
+ root = ET.parse(os.path.join(self.storedir, '_in_update', '_files')).getroot()
+ rfiles = self.__get_files(root)
+ kept, added, deleted, services = self.__get_rev_changes(rfiles)
+ # check if we aborted in the middle of a file update
+ broken_file = os.listdir(os.path.join(self.storedir, '_in_update'))
+ broken_file.remove('_files')
+ if len(broken_file) == 1:
+ origfile = os.path.join(self.storedir, '_in_update', broken_file[0])
+ wcfile = os.path.join(self.absdir, broken_file[0])
+ origfile_md5 = dgst(origfile)
+ origfile_meta = self.findfilebyname(broken_file[0])
+ if origfile.endswith('.copy'):
+ # ok it seems we aborted at some point during the copy process
+ # (copy process == copy wcfile to the _in_update dir). remove file+continue
+ os.unlink(origfile)
+ elif self.findfilebyname(broken_file[0]) is None:
+ # should we remove this file from _in_update? if we don't
+ # the user has no chance to continue without removing the file manually
+ raise oscerr.PackageInternalError(self.prjname, self.name,
+ '\'%s\' is not known by meta but exists in \'_in_update\' dir')
+ elif os.path.isfile(wcfile) and dgst(wcfile) != origfile_md5:
+ (fd, tmpfile) = tempfile.mkstemp(dir=self.absdir, prefix=broken_file[0]+'.')
+ os.close(fd)
+ os.rename(wcfile, tmpfile)
+ os.rename(origfile, wcfile)
+ print('warning: it seems you modified \'%s\' after the broken ' \
+ 'update. Restored original file and saved modified version ' \
+ 'to \'%s\'.' % (wcfile, tmpfile))
+ elif not os.path.isfile(wcfile):
+ # this is strange... because it existed before the update. restore it
+ os.rename(origfile, wcfile)
+ else:
+ # everything seems to be ok
+ os.unlink(origfile)
+ elif len(broken_file) > 1:
+ raise oscerr.PackageInternalError(self.prjname, self.name, 'too many files in \'_in_update\' dir')
+ tmp = rfiles[:]
+ for f in tmp:
+ if os.path.exists(os.path.join(self.storedir, f.name)):
+ if dgst(os.path.join(self.storedir, f.name)) == f.md5:
+ if f in kept:
+ kept.remove(f)
+ elif f in added:
+ added.remove(f)
+ # this can't happen
+ elif f in deleted:
+ deleted.remove(f)
+ if not service_files:
+ services = []
+ self.__update(kept, added, deleted, services, ET.tostring(root, encoding=ET_ENCODING), root.get('rev'))
+ os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
+ os.rmdir(os.path.join(self.storedir, '_in_update'))
+ # ok everything is ok (hopefully)...
+ fm = self.get_files_meta(revision=rev)
+ root = ET.fromstring(fm)
+ rfiles = self.__get_files(root)
+ store_write_string(self.absdir, '_files', fm + '\n', subdir='_in_update')
+ kept, added, deleted, services = self.__get_rev_changes(rfiles)
+ if not service_files:
+ services = []
+ self.__update(kept, added, deleted, services, fm, root.get('rev'))
+ os.unlink(os.path.join(self.storedir, '_in_update', '_files'))
+ if os.path.isdir(os.path.join(self.storedir, '_in_update')):
+ os.rmdir(os.path.join(self.storedir, '_in_update'))
+ self.size_limit = old_size_limit
+
+ def __update(self, kept, added, deleted, services, fm, rev):
+ pathn = getTransActPath(self.dir)
+ # check for conflicts with existing files
+ for f in added:
+ if f.name in self.filenamelist_unvers:
+ raise oscerr.PackageFileConflict(self.prjname, self.name, f.name,
+ 'failed to add file \'%s\' file/dir with the same name already exists' % f.name)
+ # ok, the update can't fail due to existing files
+ for f in added:
+ self.updatefile(f.name, rev, f.mtime)
+ print(statfrmt('A', os.path.join(pathn, f.name)))
+ for f in deleted:
+ # if the storefile doesn't exist we're resuming an aborted update:
+ # the file was already deleted but we cannot know this
+ # OR we're processing a _service: file (simply keep the file)
+ if os.path.isfile(os.path.join(self.storedir, f.name)) and self.status(f.name) != 'M':
+# if self.status(f.name) != 'M':
+ self.delete_localfile(f.name)
+ self.delete_storefile(f.name)
+ print(statfrmt('D', os.path.join(pathn, f.name)))
+ if f.name in self.to_be_deleted:
+ self.to_be_deleted.remove(f.name)
+ self.write_deletelist()
+
+ for f in kept:
+ state = self.status(f.name)
+# print f.name, state
+ if state == 'M' and self.findfilebyname(f.name).md5 == f.md5:
+ # remote file didn't change
+ pass
+ elif state == 'M':
+ # try to merge changes
+ merge_status = self.mergefile(f.name, rev, f.mtime)
+ print(statfrmt(merge_status, os.path.join(pathn, f.name)))
+ elif state == '!':
+ self.updatefile(f.name, rev, f.mtime)
+ print('Restored \'%s\'' % os.path.join(pathn, f.name))
+ elif state == 'C':
+ get_source_file(self.apiurl, self.prjname, self.name, f.name,
+ targetfilename=os.path.join(self.storedir, f.name), revision=rev,
+ progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
+ print('skipping \'%s\' (this is due to conflicts)' % f.name)
+ elif state == 'D' and self.findfilebyname(f.name).md5 != f.md5:
+ # XXX: in the worst case we might end up with f.name being
+ # in _to_be_deleted and in _in_conflict... this needs to be checked
+ if os.path.exists(os.path.join(self.absdir, f.name)):
+ merge_status = self.mergefile(f.name, rev, f.mtime)
+ print(statfrmt(merge_status, os.path.join(pathn, f.name)))
+ if merge_status == 'C':
+ # state changes from delete to conflict
+ self.to_be_deleted.remove(f.name)
+ self.write_deletelist()
+ else:
+ # XXX: we cannot recover this case because we've no file
+ # to backup
+ self.updatefile(f.name, rev, f.mtime)
+ print(statfrmt('U', os.path.join(pathn, f.name)))
+ elif state == ' ' and self.findfilebyname(f.name).md5 != f.md5:
+ self.updatefile(f.name, rev, f.mtime)
+ print(statfrmt('U', os.path.join(pathn, f.name)))
+
+ # checkout service files
+ for f in services:
+ get_source_file(self.apiurl, self.prjname, self.name, f.name,
+ targetfilename=os.path.join(self.absdir, f.name), revision=rev,
+ progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta)
+ print(statfrmt('A', os.path.join(pathn, f.name)))
+ store_write_string(self.absdir, '_files', fm + '\n')
+ if not self.meta:
+ self.update_local_pacmeta()
+ self.update_datastructs()
+
+ print('At revision %s.' % self.rev)
+
+ def run_source_services(self, mode=None, singleservice=None, verbose=None):
+ if self.name.startswith("_"):
+ return 0
+ curdir = os.getcwd()
+ os.chdir(self.absdir) # e.g. /usr/lib/obs/service/verify_file fails if not inside the project dir.
+ si = Serviceinfo()
+ if os.path.exists('_service'):
+ if self.filenamelist.count('_service') or self.filenamelist_unvers.count('_service'):
+ try:
+ service = ET.parse(os.path.join(self.absdir, '_service')).getroot()
+ except ET.ParseError as v:
+ line, column = v.position
+ print('XML error in _service file on line %s, column %s' % (line, column))
+ sys.exit(1)
+ si.read(service)
+ si.getProjectGlobalServices(self.apiurl, self.prjname, self.name)
+ r = si.execute(self.absdir, mode, singleservice, verbose)
+ os.chdir(curdir)
+ return r
+
+ def revert(self, filename):
+ if not filename in self.filenamelist and not filename in self.to_be_added:
+ raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % filename)
+ elif filename in self.skipped:
+ raise oscerr.OscIOError(None, 'file \'%s\' is marked as skipped and cannot be reverted' % filename)
+ if filename in self.filenamelist and not os.path.exists(os.path.join(self.storedir, filename)):
+ raise oscerr.PackageInternalError('file \'%s\' is listed in filenamelist but no storefile exists' % filename)
+ state = self.status(filename)
+ if not (state == 'A' or state == '!' and filename in self.to_be_added):
+ shutil.copyfile(os.path.join(self.storedir, filename), os.path.join(self.absdir, filename))
+ if state == 'D':
+ self.to_be_deleted.remove(filename)
+ self.write_deletelist()
+ elif state == 'C':
+ self.clear_from_conflictlist(filename)
+ elif state in ('A', 'R') or state == '!' and filename in self.to_be_added:
+ self.to_be_added.remove(filename)
+ self.write_addlist()
+
+ @staticmethod
+ def init_package(apiurl, project, package, dir, size_limit=None, meta=False, progress_obj=None):
+ global store
+
+ if not os.path.exists(dir):
+ os.mkdir(dir)
+ elif not os.path.isdir(dir):
+ raise oscerr.OscIOError(None, 'error: \'%s\' is no directory' % dir)
+ if os.path.exists(os.path.join(dir, store)):
+ raise oscerr.OscIOError(None, 'error: \'%s\' is already an initialized osc working copy' % dir)
+ else:
+ os.mkdir(os.path.join(dir, store))
+ store_write_project(dir, project)
+ store_write_string(dir, '_package', package + '\n')
+ store_write_apiurl(dir, apiurl)
+ if meta:
+ store_write_string(dir, '_meta_mode', '')
+ if size_limit:
+ store_write_string(dir, '_size_limit', str(size_limit) + '\n')
+ store_write_string(dir, '_files', '<directory />' + '\n')
+ store_write_string(dir, '_osclib_version', __store_version__ + '\n')
+ return Package(dir, progress_obj=progress_obj, size_limit=size_limit)
+
+
+class AbstractState:
+ """
+ Base class which represents state-like objects (<review />, <state />).
+ """
+ def __init__(self, tag):
+ self.__tag = tag
+
+ def get_node_attrs(self):
+ """return attributes for the tag/element"""
+ raise NotImplementedError()
+
+ def get_node_name(self):
+ """return tag/element name"""
+ return self.__tag
+
+ def get_comment(self):
+ """return data from <comment /> tag"""
+ raise NotImplementedError()
+
+ def get_description(self):
+ """return data from <description /> tag"""
+ raise NotImplementedError()
+
+ def to_xml(self):
+ """serialize object to XML"""
+ root = ET.Element(self.get_node_name())
+ for attr in self.get_node_attrs():
+ val = getattr(self, attr)
+ if not val is None:
+ root.set(attr, val)
+ if self.get_description():
+ ET.SubElement(root, 'description').text = self.get_description()
+ if self.get_comment():
+ ET.SubElement(root, 'comment').text = self.get_comment()
+ return root
+
+ def to_str(self):
+ """return "pretty" XML data"""
+ root = self.to_xml()
+ xmlindent(root)
+ return ET.tostring(root, encoding=ET_ENCODING)
+
+
+class ReviewState(AbstractState):
+ """Represents the review state in a request"""
+ def __init__(self, review_node):
+ if not review_node.get('state'):
+ raise oscerr.APIError('invalid review node (state attr expected): %s' % \
+ ET.tostring(review_node, encoding=ET_ENCODING))
+ AbstractState.__init__(self, review_node.tag)
+ self.state = review_node.get('state')
+ self.by_user = review_node.get('by_user')
+ self.by_group = review_node.get('by_group')
+ self.by_project = review_node.get('by_project')
+ self.by_package = review_node.get('by_package')
+ self.who = review_node.get('who')
+ self.when = review_node.get('when')
+ self.comment = ''
+ if not review_node.find('comment') is None and \
+ review_node.find('comment').text:
+ self.comment = review_node.find('comment').text.strip()
+
+ def get_node_attrs(self):
+ return ('state', 'by_user', 'by_group', 'by_project', 'by_package', 'who', 'when')
+
+ def get_comment(self):
+ return self.comment
+
+ def get_description(self):
+ return None
+
+
+class RequestHistory(AbstractState):
+ """Represents a history element of a request"""
+ re_name = re.compile(r'^Request (?:got )?([^\s]+)$')
+
+ def __init__(self, history_node):
+ AbstractState.__init__(self, history_node.tag)
+ self.who = history_node.get('who')
+ self.when = history_node.get('when')
+ if not history_node.find('description') is None and \
+ history_node.find('description').text:
+ # OBS 2.6
+ self.description = history_node.find('description').text.strip()
+ else:
+ # OBS 2.5 and before
+ self.description = history_node.get('name')
+ self.comment = ''
+ if not history_node.find('comment') is None and \
+ history_node.find('comment').text:
+ self.comment = history_node.find('comment').text.strip()
+ self.name = self._parse_name(history_node)
+
+ def _parse_name(self, history_node):
+ name = history_node.get('name', None)
+ if name is not None:
+ # OBS 2.5 and before
+ return name
+ mo = self.re_name.search(self.description)
+ if mo is not None:
+ return mo.group(1)
+ return self.description
+
+ def get_node_attrs(self):
+ return ('who', 'when')
+
+ def get_description(self):
+ return self.description
+
+ def get_comment(self):
+ return self.comment
+
+
+class RequestState(AbstractState):
+ """Represents the state of a request"""
+ def __init__(self, state_node):
+ if not state_node.get('name'):
+ raise oscerr.APIError('invalid request state node (name attr expected): %s' % \
+ ET.tostring(state_node, encoding=ET_ENCODING))
+ AbstractState.__init__(self, state_node.tag)
+ self.name = state_node.get('name')
+ self.who = state_node.get('who')
+ self.when = state_node.get('when')
+ if state_node.find('description') is None:
+ # OBS 2.6 has it always, before it did not exist
+ self.description = state_node.get('description')
+ self.comment = ''
+ if not state_node.find('comment') is None and \
+ state_node.find('comment').text:
+ self.comment = state_node.find('comment').text.strip()
+
+ def get_node_attrs(self):
+ return ('name', 'who', 'when')
+
+ def get_comment(self):
+ return self.comment
+
+ def get_description(self):
+ return None
+
+
+class Action:
+ """
+ Represents a <action /> element of a Request.
+ This class is quite common so that it can be used for all different
+ action types. Note: instances only provide attributes for their specific
+ type.
+ Examples:
+ r = Action('set_bugowner', tgt_project='foo', person_name='buguser')
+ # available attributes: r.type (== 'set_bugowner'), r.tgt_project (== 'foo'), r.tgt_package (== None)
+ r.to_str() ->
+ <action type="set_bugowner">
+ <target project="foo" />
+ <person name="buguser" />
+ </action>
+ ##
+ r = Action('delete', tgt_project='foo', tgt_package='bar')
+ # available attributes: r.type (== 'delete'), r.tgt_project (== 'foo'), r.tgt_package (=='bar')
+ r.to_str() ->
+ <action type="delete">
+ <target package="bar" project="foo" />
+ </action>
+ """
+
+ # allowed types + the corresponding (allowed) attributes
+ type_args = {'submit': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'opt_sourceupdate',
+ 'acceptinfo_rev', 'acceptinfo_srcmd5', 'acceptinfo_xsrcmd5', 'acceptinfo_osrcmd5',
+ 'acceptinfo_oxsrcmd5', 'opt_updatelink', 'opt_makeoriginolder'),
+ 'add_role': ('tgt_project', 'tgt_package', 'person_name', 'person_role', 'group_name', 'group_role'),
+ 'set_bugowner': ('tgt_project', 'tgt_package', 'person_name', 'group_name'),
+ 'maintenance_release': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'person_name',
+ 'acceptinfo_rev', 'acceptinfo_srcmd5', 'acceptinfo_xsrcmd5', 'acceptinfo_osrcmd5',
+ 'acceptinfo_oxsrcmd5', 'acceptinfo_oproject', 'acceptinfo_opackage'),
+ 'maintenance_incident': ('src_project', 'src_package', 'src_rev', 'tgt_project', 'tgt_package', 'tgt_releaseproject', 'person_name', 'opt_sourceupdate', 'opt_makeoriginolder',
+ 'acceptinfo_rev', 'acceptinfo_srcmd5', 'acceptinfo_xsrcmd5', 'acceptinfo_osrcmd5',
+ 'acceptinfo_oxsrcmd5'),
+ 'delete': ('tgt_project', 'tgt_package', 'tgt_repository'),
+ 'change_devel': ('src_project', 'src_package', 'tgt_project', 'tgt_package'),
+ 'group': ('grouped_id', )}
+ # attribute prefix to element name map (only needed for abbreviated attributes)
+ prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'}
+
+ def __init__(self, type, **kwargs):
+ if not type in Action.type_args.keys():
+ raise oscerr.WrongArgs('invalid action type: \'%s\'' % type)
+ self.type = type
+ for i in kwargs.keys():
+ if not i in Action.type_args[type]:
+ raise oscerr.WrongArgs('invalid argument: \'%s\'' % i)
+ # set all type specific attributes
+ for i in Action.type_args[type]:
+ setattr(self, i, kwargs.get(i))
+
+ def to_xml(self):
+ """
+ Serialize object to XML.
+ The xml tag names and attributes are constructed from the instance's attributes.
+ Example:
+ self.group_name -> tag name is "group", attribute name is "name"
+ self.src_project -> tag name is "source" (translated via prefix_to_elm dict),
+ attribute name is "project"
+ Attributes prefixed with "opt_" need a special handling, the resulting xml should
+ look like this: opt_updatelink -> <options><updatelink>value</updatelink></options>.
+ Attributes which are "None" will be skipped.
+ """
+ root = ET.Element('action', type=self.type)
+ for i in Action.type_args[self.type]:
+ prefix, attr = i.split('_', 1)
+ vals = getattr(self, i)
+ # single, plain elements are _not_ stored in a list
+ plain = False
+ if vals is None:
+ continue
+ elif not hasattr(vals, 'append'):
+ vals = [vals]
+ plain = True
+ for val in vals:
+ elm = root.find(Action.prefix_to_elm.get(prefix, prefix))
+ if elm is None or not plain:
+ elm = ET.Element(Action.prefix_to_elm.get(prefix, prefix))
+ root.append(elm)
+ if prefix == 'opt':
+ ET.SubElement(elm, attr).text = val
+ else:
+ elm.set(attr, val)
+ return root
+
+ def to_str(self):
+ """return "pretty" XML data"""
+ root = self.to_xml()
+ xmlindent(root)
+ return ET.tostring(root, encoding=ET_ENCODING)
+
+ @staticmethod
+ def from_xml(action_node):
+ """create action from XML"""
+ if action_node is None or \
+ not action_node.get('type') in Action.type_args.keys() or \
+ not action_node.tag in ('action', 'submit'):
+ raise oscerr.WrongArgs('invalid argument')
+ elm_to_prefix = dict([(i[1], i[0]) for i in Action.prefix_to_elm.items()])
+ kwargs = {}
+ for node in action_node:
+ prefix = elm_to_prefix.get(node.tag, node.tag)
+ if prefix == 'opt':
+ data = [('opt_%s' % opt.tag, opt.text.strip()) for opt in node if opt.text]
+ else:
+ data = [('%s_%s' % (prefix, k), v) for k, v in node.items()]
+ # it would be easier to store everything in a list but in
+ # this case we would lose some "structure" (see to_xml)
+ for k, v in data:
+ if k in kwargs:
+ l = kwargs[k]
+ if not hasattr(l, 'append'):
+ l = [l]
+ kwargs[k] = l
+ l.append(v)
+ else:
+ kwargs[k] = v
+ return Action(action_node.get('type'), **kwargs)
+
+
+class Request:
+ """Represents a request (<request />)"""
+
+ def __init__(self):
+ self._init_attributes()
+
+ def _init_attributes(self):
+ """initialize attributes with default values"""
+ self.reqid = None
+ self.title = ''
+ self.description = ''
+ self.priority = None
+ self.state = None
+ self.accept_at = None
+ self.actions = []
+ self.statehistory = []
+ self.reviews = []
+
+ def read(self, root):
+ """read in a request"""
+ self._init_attributes()
+ if not root.get('id'):
+ raise oscerr.APIError('invalid request: %s\n' % ET.tostring(root, encoding=ET_ENCODING))
+ self.reqid = root.get('id')
+ if root.find('state') is None:
+ raise oscerr.APIError('invalid request (state expected): %s\n' % ET.tostring(root, encoding=ET_ENCODING))
+ self.state = RequestState(root.find('state'))
+ action_nodes = root.findall('action')
+ if not action_nodes:
+ # check for old-style requests
+ for i in root.findall('submit'):
+ i.set('type', 'submit')
+ action_nodes.append(i)
+ for action in action_nodes:
+ self.actions.append(Action.from_xml(action))
+ for review in root.findall('review'):
+ self.reviews.append(ReviewState(review))
+ for history_element in root.findall('history'):
+ self.statehistory.append(RequestHistory(history_element))
+ if not root.find('priority') is None and root.find('priority').text:
+ self.priority = root.find('priority').text.strip()
+ if not root.find('accept_at') is None and root.find('accept_at').text:
+ self.accept_at = root.find('accept_at').text.strip()
+ if not root.find('title') is None:
+ self.title = root.find('title').text.strip()
+ if not root.find('description') is None and root.find('description').text:
+ self.description = root.find('description').text.strip()
+
+ def add_action(self, type, **kwargs):
+ """add a new action to the request"""
+ self.actions.append(Action(type, **kwargs))
+
+ def get_actions(self, *types):
+ """
+ get all actions with a specific type
+ (if types is empty return all actions)
+ """
+ if not types:
+ return self.actions
+ return [i for i in self.actions if i.type in types]
+
+ def get_creator(self):
+ """return the creator of the request"""
+ if len(self.statehistory):
+ return self.statehistory[0].who
+ return self.state.who
+
+ def to_xml(self):
+ """serialize object to XML"""
+ root = ET.Element('request')
+ if not self.reqid is None:
+ root.set('id', self.reqid)
+ for action in self.actions:
+ root.append(action.to_xml())
+ if not self.state is None:
+ root.append(self.state.to_xml())
+ for review in self.reviews:
+ root.append(review.to_xml())
+ for hist in self.statehistory:
+ root.append(hist.to_xml())
+ if self.title:
+ ET.SubElement(root, 'title').text = self.title
+ if self.description:
+ ET.SubElement(root, 'description').text = self.description
+ if self.accept_at:
+ ET.SubElement(root, 'accept_at').text = self.accept_at
+ if self.priority:
+ ET.SubElement(root, 'priority').text = self.priority
+ return root
+
+ def to_str(self):
+ """return "pretty" XML data"""
+ root = self.to_xml()
+ xmlindent(root)
+ return ET.tostring(root, encoding=ET_ENCODING)
+
+ def accept_at_in_hours(self, hours):
+ """set auto accept_at time"""
+ import datetime
+
+ now = datetime.datetime.utcnow()
+ now = now + datetime.timedelta(hours=hours)
+ self.accept_at = now.isoformat()
+
+ @staticmethod
+ def format_review(review, show_srcupdate=False):
+ """
+ format a review depending on the reviewer's type.
+ A dict which contains the formatted str's is returned.
+ """
+
+ d = {'state': '%s:' % review.state}
+ if review.by_package:
+ d['by'] = '%s/%s' % (review.by_project, review.by_package)
+ d['type'] = 'Package'
+ elif review.by_project:
+ d['by'] = '%s' % review.by_project
+ d['type'] = 'Project'
+ elif review.by_group:
+ d['by'] = '%s' % review.by_group
+ d['type'] = 'Group'
+ else:
+ d['by'] = '%s' % review.by_user
+ d['type'] = 'User'
+ if review.who:
+ d['by'] += '(%s)' % review.who
+ return d
+
+ def format_action(self, action, show_srcupdate=False):
+ """
+ format an action depending on the action's type.
+ A dict which contains the formatted str's is returned.
+ """
+ def prj_pkg_join(prj, pkg, repository=None):
+ if not pkg:
+ if not repository:
+ return prj or ''
+ return '%s(%s)' % (prj, repository)
+ return '%s/%s' % (prj, pkg)
+
+ d = {'type': '%s:' % action.type}
+ if action.type == 'set_bugowner':
+ if action.person_name:
+ d['source'] = action.person_name
+ if action.group_name:
+ d['source'] = 'group:%s' % action.group_name
+ d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
+ elif action.type == 'change_devel':
+ d['source'] = prj_pkg_join(action.tgt_project, action.tgt_package)
+ d['target'] = 'developed in %s' % prj_pkg_join(action.src_project, action.src_package)
+ elif action.type == 'maintenance_incident':
+ d['source'] = '%s ->' % action.src_project
+ if action.src_package:
+ d['source'] = '%s' % prj_pkg_join(action.src_project, action.src_package)
+ if action.src_rev:
+ d['source'] = d['source'] + '@%s' % action.src_rev
+ d['source'] = d['source'] + ' ->'
+ d['target'] = action.tgt_project
+ if action.tgt_releaseproject:
+ d['target'] += " (release in " + action.tgt_releaseproject + ")"
+ srcupdate = ' '
+ if action.opt_sourceupdate and show_srcupdate:
+ srcupdate = '(%s)' % action.opt_sourceupdate
+ elif action.type == 'maintenance_release':
+ d['source'] = '%s' % prj_pkg_join(action.src_project, action.src_package)
+ if action.src_rev:
+ d['source'] = d['source'] + '@%s' % action.src_rev
+ d['source'] = d['source'] + ' ->'
+ d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
+ elif action.type == 'submit':
+ d['source'] = '%s' % prj_pkg_join(action.src_project, action.src_package)
+ if action.src_rev:
+ d['source'] = d['source'] + '@%s' % action.src_rev
+ if action.opt_sourceupdate and show_srcupdate:
+ d['source'] = d['source'] + '(%s)' % action.opt_sourceupdate
+ d['source'] = d['source'] + ' ->'
+ tgt_package = action.tgt_package
+ if action.src_package == action.tgt_package:
+ tgt_package = ''
+ d['target'] = prj_pkg_join(action.tgt_project, tgt_package)
+ if action.opt_makeoriginolder:
+ d['target'] = d['target'] + ' ***make origin older***'
+ if action.opt_updatelink:
+ d['target'] = d['target'] + ' ***update link***'
+ elif action.type == 'add_role':
+ roles = []
+ if action.person_name and action.person_role:
+ roles.append('person: %s as %s' % (action.person_name, action.person_role))
+ if action.group_name and action.group_role:
+ roles.append('group: %s as %s' % (action.group_name, action.group_role))
+ d['source'] = ', '.join(roles)
+ d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package)
+ elif action.type == 'delete':
+ d['source'] = ''
+ d['target'] = prj_pkg_join(action.tgt_project, action.tgt_package, action.tgt_repository)
+ elif action.type == 'group':
+ l = action.grouped_id
+ if l is None:
+ # there may be no requests in a group action
+ l = ''
+ if not hasattr(l, 'append'):
+ l = [l]
+ d['source'] = ', '.join(l) + ' ->'
+ d['target'] = self.reqid
+ else:
+ raise oscerr.APIError('Unknown action type %s\n' % action.type)
+ return d
+
+ def list_view(self):
+ """return "list view" format"""
+ import textwrap
+ lines = ['%6s State:%-10s By:%-12s When:%-19s' % (self.reqid, self.state.name, self.state.who, self.state.when)]
+ tmpl = ' %(type)-16s %(source)-50s %(target)s'
+ for action in self.actions:
+ lines.append(tmpl % self.format_action(action))
+ tmpl = ' Review by %(type)-10s is %(state)-10s %(by)-50s'
+ for review in self.reviews:
+ lines.append(tmpl % Request.format_review(review))
+ history = ['%s: %s' % (hist.description, hist.who) for hist in self.statehistory]
+ if history:
+ lines.append(' From: %s' % ' -> '.join(history))
+ if self.description:
+ lines.append(textwrap.fill(self.description, width=80, initial_indent=' Descr: ',
+ subsequent_indent=' '))
+ lines.append(textwrap.fill(self.state.comment, width=80, initial_indent=' Comment: ',
+ subsequent_indent=' '))
+ return '\n'.join(lines)
+
+ def __str__(self):
+ """return "detailed" format"""
+ lines = ['Request: #%s\n' % self.reqid]
+ if self.accept_at and self.state.name in [ 'new', 'review' ]:
+ lines.append(' *** This request will get automatically accepted after '+self.accept_at+' ! ***\n')
+ if self.priority in [ 'critical', 'important' ] and self.state.name in [ 'new', 'review' ]:
+ lines.append(' *** This request has classified as '+self.priority+' ! ***\n')
+
+ for action in self.actions:
+ tmpl = ' %(type)-13s %(source)s %(target)s'
+ if action.type == 'delete':
+ # remove 1 whitespace because source is empty
+ tmpl = ' %(type)-12s %(source)s %(target)s'
+ lines.append(tmpl % self.format_action(action, show_srcupdate=True))
+ lines.append('\n\nMessage:')
+ if self.description:
+ lines.append(self.description)
+ else:
+ lines.append('<no message>')
+ if self.state:
+ lines.append('\nState: %-10s %-12s %s' % (self.state.name, self.state.when, self.state.who))
+ lines.append('Comment: %s' % (self.state.comment or '<no comment>'))
+
+ indent = '\n '
+ tmpl = '%(state)-10s %(by)-50s %(when)-12s %(who)-20s %(comment)s'
+ reviews = []
+ for review in reversed(self.reviews):
+ d = {'state': review.state}
+ if review.by_user:
+ d['by'] = "User: " + review.by_user
+ if review.by_group:
+ d['by'] = "Group: " + review.by_group
+ if review.by_package:
+ d['by'] = "Package: " + review.by_project + "/" + review.by_package
+ elif review.by_project:
+ d['by'] = "Project: " + review.by_project
+ d['when'] = review.when or ''
+ d['who'] = review.who or ''
+ d['comment'] = ''
+ if review.comment:
+ d['comment'] = '\n ' + review.comment
+ reviews.append(tmpl % d)
+ if reviews:
+ lines.append('\nReview: %s' % indent.join(reviews))
+
+ tmpl = '%(when)-10s %(who)-12s %(desc)s'
+ histories = []
+ for hist in reversed(self.statehistory):
+ d = {'when': hist.when, 'who': hist.who, 'desc': hist.description}
+ histories.append(tmpl % d)
+ if histories:
+ lines.append('\nHistory: %s' % indent.join(histories))
+
+ return '\n'.join(lines)
+
+ def __cmp__(self, other):
+ return cmp(int(self.reqid), int(other.reqid))
+
+ def create(self, apiurl, addrevision=False):
+ """create a new request"""
+ query = {'cmd' : 'create' }
+ if addrevision:
+ query['addrevision'] = "1"
+ u = makeurl(apiurl, ['request'], query=query)
+ f = http_POST(u, data=self.to_str())
+ root = ET.fromstring(f.read())
+ self.read(root)
+
+def shorttime(t):
+ """format time as Apr 02 18:19
+ or Apr 02 2005
+ depending on whether it is in the current year
+ """
+ import time
+
+ if time.gmtime()[0] == time.gmtime(t)[0]:
+ # same year
+ return time.strftime('%b %d %H:%M', time.gmtime(t))
+ else:
+ return time.strftime('%b %d %Y', time.gmtime(t))
+
+
+def is_project_dir(d):
+ global store
+
+ return os.path.exists(os.path.join(d, store, '_project')) and not \
+ os.path.exists(os.path.join(d, store, '_package'))
+
+
+def is_package_dir(d):
+ global store
+
+ return os.path.exists(os.path.join(d, store, '_project')) and \
+ os.path.exists(os.path.join(d, store, '_package'))
+
+def parse_disturl(disturl):
+ """Parse a disturl, returns tuple (apiurl, project, source, repository,
+ revision), else raises an oscerr.WrongArgs exception
+ """
+
+ global DISTURL_RE
+
+ m = DISTURL_RE.match(disturl)
+ if not m:
+ raise oscerr.WrongArgs("`%s' does not look like disturl" % disturl)
+
+ apiurl = m.group('apiurl')
+ if apiurl.split('.')[0] != 'api':
+ apiurl = 'https://api.' + ".".join(apiurl.split('.')[1:])
+ return (apiurl, m.group('project'), m.group('source'), m.group('repository'), m.group('revision'))
+
+def parse_buildlogurl(buildlogurl):
+ """Parse a build log url, returns a tuple (apiurl, project, package,
+ repository, arch), else raises oscerr.WrongArgs exception"""
+
+ global BUILDLOGURL_RE
+
+ m = BUILDLOGURL_RE.match(buildlogurl)
+ if not m:
+ raise oscerr.WrongArgs('\'%s\' does not look like url with a build log' % buildlogurl)
+
+ return (m.group('apiurl'), m.group('project'), m.group('package'), m.group('repository'), m.group('arch'))
+
+def slash_split(l):
+ """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
+ This is handy to allow copy/paste a project/package combination in this form.
+
+ Trailing slashes are removed before the split, because the split would
+ otherwise give an additional empty string.
+ """
+ r = []
+ for i in l:
+ i = i.rstrip('/')
+ r += i.split('/')
+ return r
+
+def expand_proj_pack(args, idx=0, howmany=0):
+ """looks for occurance of '.' at the position idx.
+ If howmany is 2, both proj and pack are expanded together
+ using the current directory, or none of them if not possible.
+ If howmany is 0, proj is expanded if possible, then, if there
+ is no idx+1 element in args (or args[idx+1] == '.'), pack is also
+ expanded, if possible.
+ If howmany is 1, only proj is expanded if possible.
+
+ If args[idx] does not exist, an implicit '.' is assumed.
+ If not enough elements up to idx exist, an error is raised.
+
+ See also parseargs(args), slash_split(args), findpacs(args)
+ All these need unification, somehow.
+ """
+
+ # print args,idx,howmany
+
+ if len(args) < idx:
+ raise oscerr.WrongArgs('not enough argument, expected at least %d' % idx)
+
+ if len(args) == idx:
+ args += '.'
+ if args[idx+0] == '.':
+ if howmany == 0 and len(args) > idx+1:
+ if args[idx+1] == '.':
+ # we have two dots.
+ # remove one dot and make sure to expand both proj and pack
+ args.pop(idx+1)
+ howmany = 2
+ else:
+ howmany = 1
+ # print args,idx,howmany
+
+ args[idx+0] = store_read_project('.')
+ if howmany == 0:
+ try:
+ package = store_read_package('.')
+ args.insert(idx+1, package)
+ except:
+ pass
+ elif howmany == 2:
+ package = store_read_package('.')
+ args.insert(idx+1, package)
+ return args
+
+
+def findpacs(files, progress_obj=None):
+ """collect Package objects belonging to the given files
+ and make sure each Package is returned only once"""
+ pacs = []
+ for f in files:
+ p = filedir_to_pac(f, progress_obj)
+ known = None
+ for i in pacs:
+ if i.name == p.name:
+ known = i
+ break
+ if known:
+ i.merge(p)
+ else:
+ pacs.append(p)
+ return pacs
+
+
+def filedir_to_pac(f, progress_obj=None):
+ """Takes a working copy path, or a path to a file inside a working copy,
+ and returns a Package object instance
+
+ If the argument was a filename, add it onto the "todo" list of the Package """
+
+ if os.path.isdir(f):
+ wd = f
+ p = Package(wd, progress_obj=progress_obj)
+ else:
+ wd = os.path.dirname(f) or os.curdir
+ p = Package(wd, progress_obj=progress_obj)
+ p.todo = [ os.path.basename(f) ]
+ return p
+
+
+def read_filemeta(dir):
+ global store
+
+ msg = '\'%s\' is not a valid working copy.' % dir
+ filesmeta = os.path.join(dir, store, '_files')
+ if not is_package_dir(dir):
+ raise oscerr.NoWorkingCopy(msg)
+ if not os.path.isfile(filesmeta):
+ raise oscerr.NoWorkingCopy('%s (%s does not exist)' % (msg, filesmeta))
+
+ try:
+ r = ET.parse(filesmeta)
+ except SyntaxError as e:
+ raise oscerr.NoWorkingCopy('%s\nWhen parsing .osc/_files, the following error was encountered:\n%s' % (msg, e))
+ return r
+
+def store_readlist(dir, name):
+ global store
+
+ r = []
+ if os.path.exists(os.path.join(dir, store, name)):
+ r = [line.rstrip('\n') for line in open(os.path.join(dir, store, name), 'r')]
+ return r
+
+def read_tobeadded(dir):
+ return store_readlist(dir, '_to_be_added')
+
+def read_tobedeleted(dir):
+ return store_readlist(dir, '_to_be_deleted')
+
+def read_sizelimit(dir):
+ global store
+
+ r = None
+ fname = os.path.join(dir, store, '_size_limit')
+
+ if os.path.exists(fname):
+ r = open(fname).readline().strip()
+
+ if r is None or not r.isdigit():
+ return None
+ return int(r)
+
+def read_inconflict(dir):
+ return store_readlist(dir, '_in_conflict')
+
+def parseargs(list_of_args):
+ """Convenience method osc's commandline argument parsing.
+
+ If called with an empty tuple (or list), return a list containing the current directory.
+ Otherwise, return a list of the arguments."""
+ if list_of_args:
+ return list(list_of_args)
+ else:
+ return [os.curdir]
+
+
+def statfrmt(statusletter, filename):
+ return '%s %s' % (statusletter, filename)
+
+
+def pathjoin(a, *p):
+ """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
+ path = os.path.join(a, *p)
+ if path.startswith('./'):
+ path = path[2:]
+ return path
+
+
+def makeurl(baseurl, l, query=[]):
+ """Given a list of path compoments, construct a complete URL.
+
+ Optional parameters for a query string can be given as a list, as a
+ dictionary, or as an already assembled string.
+ In case of a dictionary, the parameters will be urlencoded by this
+ function. In case of a list not -- this is to be backwards compatible.
+ """
+
+ if conf.config['verbose'] > 1:
+ print('makeurl:', baseurl, l, query)
+
+ if isinstance(query, type(list())):
+ query = '&'.join(query)
+ elif isinstance(query, type(dict())):
+ query = urlencode(query)
+
+ scheme, netloc, path = urlsplit(baseurl)[0:3]
+ return urlunsplit((scheme, netloc, '/'.join([path] + list(l)), query, ''))
+
+
+def http_request(method, url, headers={}, data=None, file=None):
+ """wrapper around urllib2.urlopen for error handling,
+ and to support additional (PUT, DELETE) methods"""
+ def create_memoryview(obj):
+ if sys.version_info < (2, 7, 99):
+ # obj might be a mmap and python 2.7's mmap does not
+ # behave like a bytearray (a bytearray in turn can be used
+ # to create the memoryview). For now simply return a buffer
+ return buffer(obj)
+ return memoryview(obj)
+
+ filefd = None
+
+ if conf.config['http_debug']:
+ print('\n\n--', method, url, file=sys.stderr)
+
+ if method == 'POST' and not file and not data:
+ # adding data to an urllib2 request transforms it into a POST
+ data = ''
+
+ req = URLRequest(url)
+ api_host_options = {}
+ apiurl = conf.extract_known_apiurl(url)
+ if apiurl is not None:
+ # ok no external request
+ install_opener(conf._build_opener(apiurl))
+ api_host_options = conf.get_apiurl_api_host_options(apiurl)
+ for header, value in api_host_options['http_headers']:
+ req.add_header(header, value)
+
+ req.get_method = lambda: method
+
+ # POST requests are application/x-www-form-urlencoded per default
+ # but sending data requires an octet-stream type
+ if method == 'PUT' or (method == 'POST' and (data or file)):
+ req.add_header('Content-Type', 'application/octet-stream')
+
+ if isinstance(headers, type({})):
+ for i in headers.keys():
+ print(headers[i])
+ req.add_header(i, headers[i])
+
+ if file and not data:
+ size = os.path.getsize(file)
+ if size < 1024*512:
+ data = open(file, 'rb').read()
+ else:
+ import mmap
+ filefd = open(file, 'rb')
+ try:
+ if sys.platform[:3] != 'win':
+ data = mmap.mmap(filefd.fileno(), os.path.getsize(file), mmap.MAP_SHARED, mmap.PROT_READ)
+ else:
+ data = mmap.mmap(filefd.fileno(), os.path.getsize(file))
+ data = create_memoryview(data)
+ except EnvironmentError as e:
+ if e.errno == 19:
+ sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
+ '\non a filesystem which does not support this.' % (e, file))
+ elif hasattr(e, 'winerror') and e.winerror == 5:
+ # falling back to the default io
+ data = open(file, 'rb').read()
+ else:
+ raise
+
+ if conf.config['debug']: print(method, url, file=sys.stderr)
+
+ try:
+ if isinstance(data, str):
+ data = bytes(data, "utf-8")
+ fd = urlopen(req, data=data)
+
+ finally:
+ if hasattr(conf.cookiejar, 'save'):
+ conf.cookiejar.save(ignore_discard=True)
+
+ if filefd: filefd.close()
+
+ return fd
+
+
+def http_GET(*args, **kwargs): return http_request('GET', *args, **kwargs)
+def http_POST(*args, **kwargs): return http_request('POST', *args, **kwargs)
+def http_PUT(*args, **kwargs): return http_request('PUT', *args, **kwargs)
+def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
+
+
+def check_store_version(dir):
+ global store
+
+ versionfile = os.path.join(dir, store, '_osclib_version')
+ try:
+ v = open(versionfile).read().strip()
+ except:
+ v = ''
+
+ if v == '':
+ msg = 'Error: "%s" is not an osc package working copy.' % os.path.abspath(dir)
+ if os.path.exists(os.path.join(dir, '.svn')):
+ msg = msg + '\nTry svn instead of osc.'
+ raise oscerr.NoWorkingCopy(msg)
+
+ if v != __store_version__:
+ if v in ['0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '0.95', '0.96', '0.97', '0.98', '0.99']:
+ # version is fine, no migration needed
+ f = open(versionfile, 'w')
+ f.write(__store_version__ + '\n')
+ f.close()
+ return
+ msg = 'The osc metadata of your working copy "%s"' % dir
+ msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
+ msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
+ raise oscerr.WorkingCopyWrongVersion(msg)
+
+
+def meta_get_packagelist(apiurl, prj, deleted=None, expand=False):
+
+ query = {}
+ if deleted:
+ query['deleted'] = 1
+ if expand:
+ query['expand'] = 1
+
+ u = makeurl(apiurl, ['source', prj], query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ return [ node.get('name') for node in root.findall('entry') ]
+
+
+def meta_get_filelist(apiurl, prj, package, verbose=False, expand=False, revision=None, meta=False, deleted=False):
+ """return a list of file names,
+ or a list File() instances if verbose=True"""
+
+ query = {}
+ if deleted:
+ query['deleted'] = 1
+ if expand:
+ query['expand'] = 1
+ if meta:
+ query['meta'] = 1
+ if revision:
+ query['rev'] = revision
+ else:
+ query['rev'] = 'latest'
+
+ u = makeurl(apiurl, ['source', prj, package], query=query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+
+ if not verbose:
+ return [ node.get('name') for node in root.findall('entry') ]
+
+ else:
+ l = []
+ # rev = int(root.get('rev')) # don't force int. also allow srcmd5 here.
+ rev = root.get('rev')
+ for node in root.findall('entry'):
+ f = File(node.get('name'),
+ node.get('md5'),
+ int(node.get('size')),
+ int(node.get('mtime')))
+ f.rev = rev
+ l.append(f)
+ return l
+
+
+def meta_get_project_list(apiurl, deleted=None):
+ query = {}
+ if deleted:
+ query['deleted'] = 1
+
+ u = makeurl(apiurl, ['source'], query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+ return sorted([ node.get('name') for node in root if node.get('name')])
+
+
+def show_project_meta(apiurl, prj):
+ url = makeurl(apiurl, ['source', prj, '_meta'])
+ f = http_GET(url)
+ return f.readlines()
+
+
+def show_project_conf(apiurl, prj):
+ url = makeurl(apiurl, ['source', prj, '_config'])
+ f = http_GET(url)
+ return f.readlines()
+
+
+def show_package_trigger_reason(apiurl, prj, pac, repo, arch):
+ url = makeurl(apiurl, ['build', prj, repo, arch, pac, '_reason'])
+ try:
+ f = http_GET(url)
+ return f.read()
+ except HTTPError as e:
+ e.osc_msg = 'Error getting trigger reason for project \'%s\' package \'%s\'' % (prj, pac)
+ raise
+
+
+def show_package_meta(apiurl, prj, pac, meta=False):
+ query = {}
+ if meta:
+ query['meta'] = 1
+
+ # The fake packages _project has no _meta file
+ if pac.startswith('_project'):
+ return ""
+
+ url = makeurl(apiurl, ['source', prj, pac, '_meta'], query)
+ try:
+ f = http_GET(url)
+ return f.readlines()
+ except HTTPError as e:
+ e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
+ raise
+
+
+def show_attribute_meta(apiurl, prj, pac, subpac, attribute, with_defaults, with_project):
+ path = []
+ path.append('source')
+ path.append(prj)
+ if pac:
+ path.append(pac)
+ if pac and subpac:
+ path.append(subpac)
+ path.append('_attribute')
+ if attribute:
+ path.append(attribute)
+ query = []
+ if with_defaults:
+ query.append("with_default=1")
+ if with_project:
+ query.append("with_project=1")
+ url = makeurl(apiurl, path, query)
+ try:
+ f = http_GET(url)
+ return f.readlines()
+ except HTTPError as e:
+ e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
+ raise
+
+
+def show_devel_project(apiurl, prj, pac):
+ m = show_package_meta(apiurl, prj, pac)
+ node = ET.fromstring(''.join(m)).find('devel')
+ if node is None:
+ return None, None
+ else:
+ return node.get('project'), node.get('package', None)
+
+
+def set_devel_project(apiurl, prj, pac, devprj=None, devpac=None):
+ meta = show_package_meta(apiurl, prj, pac)
+ root = ET.fromstring(''.join(meta))
+ node = root.find('devel')
+ if node is None:
+ if devprj is None:
+ return
+ node = ET.Element('devel')
+ root.append(node)
+ else:
+ if devprj is None:
+ root.remove(node)
+ else:
+ node.clear()
+ if devprj:
+ node.set('project', devprj)
+ if devpac:
+ node.set('package', devpac)
+ url = makeurl(apiurl, ['source', prj, pac, '_meta'])
+ mf = metafile(url, ET.tostring(root, encoding=ET_ENCODING))
+ mf.sync()
+
+
+def show_package_disabled_repos(apiurl, prj, pac):
+ m = show_package_meta(apiurl, prj, pac)
+ #FIXME: don't work if all repos of a project are disabled and only some are enabled since <disable/> is empty
+ try:
+ root = ET.fromstring(''.join(m))
+ elm = root.find('build')
+ r = [ node.get('repository') for node in elm.findall('disable')]
+ return r
+ except:
+ return None
+
+
+def show_pattern_metalist(apiurl, prj):
+ url = makeurl(apiurl, ['source', prj, '_pattern'])
+ try:
+ f = http_GET(url)
+ tree = ET.parse(f)
+ except HTTPError as e:
+ e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
+ raise
+ r = sorted([ node.get('name') for node in tree.getroot() ])
+ return r
+
+
+def show_pattern_meta(apiurl, prj, pattern):
+ url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
+ try:
+ f = http_GET(url)
+ return f.readlines()
+ except HTTPError as e:
+ e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
+ raise
+
+def show_configuration(apiurl):
+ u = makeurl(apiurl, ['public', 'configuration'])
+ f = http_GET(u)
+ return f.readlines()
+
+
+class metafile:
+ """metafile that can be manipulated and is stored back after manipulation."""
+ def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
+ import tempfile
+
+ self.url = url
+ self.change_is_required = change_is_required
+ (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext)
+ f = os.fdopen(fd, 'w')
+ f.write(''.join(input))
+ f.close()
+ self.hash_orig = dgst(self.filename)
+
+ def sync(self):
+ if self.change_is_required and self.hash_orig == dgst(self.filename):
+ print('File unchanged. Not saving.')
+ os.unlink(self.filename)
+ return
+
+ print('Sending meta data...')
+ # don't do any exception handling... it's up to the caller what to do in case
+ # of an exception
+ http_PUT(self.url, file=self.filename)
+ os.unlink(self.filename)
+ print('Done.')
+
+ def edit(self):
+ try:
+ while True:
+ run_editor(self.filename)
+ try:
+ self.sync()
+ break
+ except HTTPError as e:
+ error_help = "%d" % e.code
+ if e.hdrs.get('X-Opensuse-Errorcode'):
+ error_help = "%s (%d)" % (e.hdrs.get('X-Opensuse-Errorcode'), e.code)
+
+ print('BuildService API error:', error_help, file=sys.stderr)
+ # examine the error - we can't raise an exception because we might want
+ # to try again
+ data = e.read()
+ if '<summary>' in data:
+ print(data.split('<summary>')[1].split('</summary>')[0], file=sys.stderr)
+ ri = raw_input('Try again? ([y/N]): ')
+ if ri not in ['y', 'Y']:
+ break
+ finally:
+ self.discard()
+
+ def discard(self):
+ if os.path.exists(self.filename):
+ print('discarding %s' % self.filename)
+ os.unlink(self.filename)
+
+# different types of metadata
+metatypes = { 'prj': { 'path': 'source/%s/_meta',
+ 'template': new_project_templ,
+ 'file_ext': '.xml'
+ },
+ 'pkg': { 'path' : 'source/%s/%s/_meta',
+ 'template': new_package_templ,
+ 'file_ext': '.xml'
+ },
+ 'attribute': { 'path' : 'source/%s/%s/_meta',
+ 'template': new_attribute_templ,
+ 'file_ext': '.xml'
+ },
+ 'prjconf': { 'path': 'source/%s/_config',
+ 'template': '',
+ 'file_ext': '.txt'
+ },
+ 'user': { 'path': 'person/%s',
+ 'template': new_user_template,
+ 'file_ext': '.xml'
+ },
+ 'pattern': { 'path': 'source/%s/_pattern/%s',
+ 'template': new_pattern_template,
+ 'file_ext': '.xml'
+ },
+ }
+
+def meta_exists(metatype,
+ path_args=None,
+ template_args=None,
+ create_new=True,
+ apiurl=None):
+
+ global metatypes
+
+ if not apiurl:
+ apiurl = conf.config['apiurl']
+ url = make_meta_url(metatype, path_args, apiurl)
+ try:
+ data = http_GET(url).readlines()
+ except HTTPError as e:
+ if e.code == 404 and create_new:
+ data = metatypes[metatype]['template']
+ if template_args:
+ data = StringIO(data % template_args).readlines()
+ else:
+ raise e
+
+ return data
+
+def make_meta_url(metatype, path_args=None, apiurl=None, force=False, remove_linking_repositories=False):
+ global metatypes
+
+ if not apiurl:
+ apiurl = conf.config['apiurl']
+ if metatype not in metatypes.keys():
+ raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
+ path = metatypes[metatype]['path']
+
+ if path_args:
+ path = path % path_args
+
+ query = {}
+ if force:
+ query = { 'force': '1' }
+ if remove_linking_repositories:
+ query['remove_linking_repositories'] = '1'
+
+ return makeurl(apiurl, [path], query)
+
+
+def edit_meta(metatype,
+ path_args=None,
+ data=None,
+ template_args=None,
+ edit=False,
+ force=False,
+ remove_linking_repositories=False,
+ change_is_required=False,
+ apiurl=None):
+
+ global metatypes
+
+ if not apiurl:
+ apiurl = conf.config['apiurl']
+ if not data:
+ data = meta_exists(metatype,
+ path_args,
+ template_args,
+ create_new = metatype != 'prjconf', # prjconf always exists, 404 => unknown prj
+ apiurl=apiurl)
+
+ if edit:
+ change_is_required = True
+
+ if metatype == 'pkg':
+ # check if the package is a link to a different project
+ project, package = path_args
+ orgprj = ET.fromstring(''.join(data)).get('project')
+ if orgprj is not None and unquote(project) != orgprj:
+ print('The package is linked from a different project.')
+ print('If you want to edit the meta of the package create first a branch.')
+ print(' osc branch %s %s %s' % (orgprj, package, unquote(project)))
+ print(' osc meta pkg %s %s -e' % (unquote(project), package))
+ return
+
+ url = make_meta_url(metatype, path_args, apiurl, force, remove_linking_repositories)
+ f = metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
+
+ if edit:
+ f.edit()
+ else:
+ f.sync()
+
+
+def show_files_meta(apiurl, prj, pac, revision=None, expand=False, linkrev=None, linkrepair=False, meta=False):
+ query = {}
+ if revision:
+ query['rev'] = revision
+ else:
+ query['rev'] = 'latest'
+ if linkrev:
+ query['linkrev'] = linkrev
+ elif conf.config['linkcontrol']:
+ query['linkrev'] = 'base'
+ if meta:
+ query['meta'] = 1
+ if expand:
+ query['expand'] = 1
+ if linkrepair:
+ query['emptylink'] = 1
+ f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query))
+ return f.read()
+
+def show_upstream_srcmd5(apiurl, prj, pac, expand=False, revision=None, meta=False, include_service_files=False):
+ m = show_files_meta(apiurl, prj, pac, expand=expand, revision=revision, meta=meta)
+ et = ET.fromstring(''.join(m))
+ if include_service_files:
+ try:
+ sinfo = et.find('serviceinfo')
+ if sinfo != None and sinfo.get('xsrcmd5') and not sinfo.get('error'):
+ return sinfo.get('xsrcmd5')
+ except:
+ pass
+ return et.get('srcmd5')
+
+
+def show_upstream_xsrcmd5(apiurl, prj, pac, revision=None, linkrev=None, linkrepair=False, meta=False, include_service_files=False):
+ m = show_files_meta(apiurl, prj, pac, revision=revision, linkrev=linkrev, linkrepair=linkrepair, meta=meta, expand=include_service_files)
+ et = ET.fromstring(''.join(m))
+ if include_service_files:
+ return et.get('srcmd5')
+
+ li_node = et.find('linkinfo')
+ if li_node is None:
+ return None
+
+ li = Linkinfo()
+ li.read(li_node)
+
+ if li.haserror():
+ raise oscerr.LinkExpandError(prj, pac, li.error)
+ return li.xsrcmd5
+
+
+def show_project_sourceinfo(apiurl, project, nofilename, *packages):
+ query = ['view=info']
+ if packages:
+ query.extend(['package=%s' % quote_plus(p) for p in packages])
+ if nofilename:
+ query.append('nofilename=1')
+ f = http_GET(makeurl(apiurl, ['source', project], query=query))
+ return f.read()
+
+
+def get_project_sourceinfo(apiurl, project, nofilename, *packages):
+ try:
+ si = show_project_sourceinfo(apiurl, project, nofilename, *packages)
+ except HTTPError as e:
+ # old API servers (e.g. 2.3.5) do not know the 'nofilename' parameter, so retry without
+ if e.code == 400 and nofilename:
+ return get_project_sourceinfo(apiurl, project, False, *packages)
+ if e.code != 414:
+ raise
+ if len(packages) == 1:
+ raise oscerr.APIError('package name too long: %s' % packages[0])
+ n = len(packages) / 2
+ pkgs = packages[:n]
+ res = get_project_sourceinfo(apiurl, project, nofilename, *pkgs)
+ pkgs = packages[n:]
+ res.update(get_project_sourceinfo(apiurl, project, nofilename, *pkgs))
+ return res
+ root = ET.fromstring(si)
+ res = {}
+ for sinfo in root.findall('sourceinfo'):
+ res[sinfo.get('package')] = sinfo
+ return res
+
+
+def show_upstream_rev_vrev(apiurl, prj, pac, revision=None, expand=False, meta=False):
+ m = show_files_meta(apiurl, prj, pac, revision=revision, expand=expand, meta=meta)
+ et = ET.fromstring(''.join(m))
+ return et.get('rev'), et.get('vrev')
+
+def show_upstream_rev(apiurl, prj, pac, revision=None, expand=False, linkrev=None, meta=False, include_service_files=False):
+ m = show_files_meta(apiurl, prj, pac, revision=revision, expand=expand, linkrev=linkrev, meta=meta)
+ et = ET.fromstring(''.join(m))
+ if include_service_files:
+ try:
+ sinfo = et.find('serviceinfo')
+ if sinfo != None and sinfo.get('xsrcmd5') and not sinfo.get('error'):
+ return sinfo.get('xsrcmd5')
+ except:
+ pass
+ return et.get('rev')
+
+
+def read_meta_from_spec(specfile, *args):
+ import codecs, re
+ """
+ Read tags and sections from spec file. To read out
+ a tag the passed argument mustn't end with a colon. To
+ read out a section the passed argument must start with
+ a '%'.
+ This method returns a dictionary which contains the
+ requested data.
+ """
+
+ if not os.path.isfile(specfile):
+ raise oscerr.OscIOError(None, '\'%s\' is not a regular file' % specfile)
+
+ try:
+ lines = codecs.open(specfile, 'r', locale.getpreferredencoding()).readlines()
+ except UnicodeDecodeError:
+ lines = open(specfile).readlines()
+
+ tags = []
+ sections = []
+ spec_data = {}
+
+ for itm in args:
+ if itm.startswith('%'):
+ sections.append(itm)
+ else:
+ tags.append(itm)
+
+ tag_pat = '(?P<tag>^%s)\s*:\s*(?P<val>.*)'
+ for tag in tags:
+ m = re.compile(tag_pat % tag, re.I | re.M).search(''.join(lines))
+ if m and m.group('val'):
+ spec_data[tag] = m.group('val').strip()
+
+ section_pat = '^%s\s*?$'
+ for section in sections:
+ m = re.compile(section_pat % section, re.I | re.M).search(''.join(lines))
+ if m:
+ start = lines.index(m.group()+'\n') + 1
+ data = []
+ for line in lines[start:]:
+ if line.startswith('%'):
+ break
+ data.append(line)
+ spec_data[section] = data
+
+ return spec_data
+
+def get_default_editor():
+ import platform
+ system = platform.system()
+ if system == 'Windows':
+ return 'notepad'
+ if system == 'Linux':
+ try:
+ # Python 2.6
+ dist = platform.linux_distribution()[0]
+ except AttributeError:
+ dist = platform.dist()[0]
+ if dist == 'debian':
+ return 'editor'
+ elif dist == 'fedora':
+ return 'vi'
+ return 'vim'
+ return 'vi'
+
+def get_default_pager():
+ import platform
+ system = platform.system()
+ if system == 'Windows':
+ return 'less'
+ if system == 'Linux':
+ try:
+ # Python 2.6
+ dist = platform.linux_distribution()[0]
+ except AttributeError:
+ dist = platform.dist()[0]
+ if dist == 'debian':
+ return 'pager'
+ return 'less'
+ return 'more'
+
+def run_pager(message, tmp_suffix=''):
+ import tempfile, sys
+
+ if not message:
+ return
+
+ if not sys.stdout.isatty():
+ print(message)
+ else:
+ tmpfile = tempfile.NamedTemporaryFile(suffix=tmp_suffix)
+ tmpfile.write(message)
+ tmpfile.flush()
+ pager = os.getenv('PAGER', default=get_default_pager())
+ try:
+ run_external(pager, tmpfile.name)
+ finally:
+ tmpfile.close()
+
+def run_editor(filename):
+ cmd = _editor_command()
+ cmd.append(filename)
+ return run_external(cmd[0], *cmd[1:])
+
+def _editor_command():
+ editor = os.getenv('EDITOR', default=get_default_editor())
+ try:
+ cmd = shlex.split(editor)
+ except SyntaxError:
+ cmd = editor.split()
+ return cmd
+
+def _edit_message_open_editor(filename, data, orig_mtime):
+ # FIXME: import modules globally
+ import tempfile
+ editor = _editor_command()
+ mtime = os.stat(filename).st_mtime
+ if mtime == orig_mtime:
+ # prepare file for editors
+ if editor[0] in ('vi', 'vim'):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(data)
+ f.flush()
+ editor.extend(['-c', ':r %s' % f.name, filename])
+ run_external(editor[0], *editor[1:])
+ else:
+ with open(filename, 'w') as f:
+ f.write(data)
+ orig_mtime = os.stat(filename).st_mtime
+ run_editor(filename)
+ else:
+ run_editor(filename)
+ return os.stat(filename).st_mtime != orig_mtime
+
+def edit_message(footer='', template='', templatelen=30):
+ delim = '--This line, and those below, will be ignored--\n'
+ data = ''
+ if template != '':
+ if not templatelen is None:
+ lines = template.splitlines()
+ data = '\n'.join(lines[:templatelen])
+ if lines[templatelen:]:
+ footer = '%s\n\n%s' % ('\n'.join(lines[templatelen:]), footer)
+ data += '\n' + delim + '\n' + footer
+ return edit_text(data, delim, suffix='.diff', template=template)
+
+def edit_text(data='', delim=None, suffix='.txt', template=''):
+ import tempfile
+ try:
+ (fd, filename) = tempfile.mkstemp(prefix='osc-editor', suffix=suffix)
+ os.close(fd)
+ mtime = os.stat(filename).st_mtime
+ while True:
+ file_changed = _edit_message_open_editor(filename, data, mtime)
+ msg = open(filename).read()
+ if delim:
+ msg = msg.split(delim)[0].rstrip()
+ if msg and file_changed:
+ break
+ else:
+ reason = 'Log message not specified'
+ if template == msg:
+ reason = 'Default log message was not changed. Press \'c\' to continue.'
+ ri = raw_input('%s\na)bort, c)ontinue, e)dit: ' % reason)
+ if ri in 'aA':
+ raise oscerr.UserAbort()
+ elif ri in 'cC':
+ break
+ elif ri in 'eE':
+ pass
+ finally:
+ os.unlink(filename)
+ return msg
+
+def clone_request(apiurl, reqid, msg=None):
+ query = {'cmd': 'branch', 'request': reqid}
+ url = makeurl(apiurl, ['source'], query)
+ r = http_POST(url, data=msg)
+ root = ET.fromstring(r.read())
+ project = None
+ for i in root.findall('data'):
+ if i.get('name') == 'targetproject':
+ project = i.text.strip()
+ if not project:
+ raise oscerr.APIError('invalid data from clone request:\n%s\n' % ET.tostring(root, encoding=ET_ENCODING))
+ return project
+
+# create a maintenance release request
+def create_release_request(apiurl, src_project, message=''):
+ import cgi
+ r = Request()
+ # api will complete the request
+ r.add_action('maintenance_release', src_project=src_project)
+ # XXX: clarify why we need the unicode(...) stuff
+ r.description = cgi.escape(unicode(message, 'utf8'))
+ r.create(apiurl)
+ return r
+
+# create a maintenance incident per request
+def create_maintenance_request(apiurl, src_project, src_packages, tgt_project, tgt_releaseproject, opt_sourceupdate, message=''):
+ import cgi
+ r = Request()
+ if src_packages:
+ for p in src_packages:
+ r.add_action('maintenance_incident', src_project=src_project, src_package=p, tgt_project=tgt_project, tgt_releaseproject=tgt_releaseproject, opt_sourceupdate = opt_sourceupdate)
+ else:
+ r.add_action('maintenance_incident', src_project=src_project, tgt_project=tgt_project, tgt_releaseproject=tgt_releaseproject, opt_sourceupdate = opt_sourceupdate)
+ # XXX: clarify why we need the unicode(...) stuff
+ r.description = cgi.escape(unicode(message, 'utf8'))
+ r.create(apiurl, addrevision=True)
+ return r
+
+def create_submit_request(apiurl,
+ src_project, src_package=None,
+ dst_project=None, dst_package=None,
+ message="", orev=None, src_update=None, dst_updatelink=None):
+
+ import cgi
+ options_block = ""
+ package = ""
+ if src_package:
+ package = """package="%s" """ % (src_package)
+ options_block = "<options>"
+ if src_update:
+ options_block += """<sourceupdate>%s</sourceupdate>""" % (src_update)
+ if dst_updatelink:
+ options_block += """<updatelink>true</updatelink>"""
+ options_block += "</options>"
+
+
+ # Yes, this kind of xml construction is horrible
+ targetxml = ""
+ if dst_project:
+ packagexml = ""
+ if dst_package:
+ packagexml = """package="%s" """ % ( dst_package )
+ targetxml = """<target project="%s" %s /> """ % ( dst_project, packagexml )
+ # XXX: keep the old template for now in order to work with old obs instances
+ xml = """\
+<request>
+ <action type="submit">
+ <source project="%s" %s rev="%s"/>
+ %s
+ %s
+ </action>
+ <state name="new"/>
+ <description>%s</description>
+</request>
+""" % (src_project,
+ package,
+ orev or show_upstream_rev(apiurl, src_project, src_package),
+ targetxml,
+ options_block,
+ cgi.escape(message))
+
+ # Don't do cgi.escape(unicode(message, "utf8"))) above.
+ # Promoting the string to utf8, causes the post to explode with:
+ # uncaught exception: Fatal error: Start tag expected, '&lt;' not found at :1.
+ # I guess, my original workaround was not that bad.
+
+ u = makeurl(apiurl, ['request'], query='cmd=create')
+ r = None
+ try:
+ f = http_POST(u, data=xml)
+ root = ET.parse(f).getroot()
+ r = root.get('id')
+ except HTTPError as e:
+ if e.hdrs.get('X-Opensuse-Errorcode') == "submit_request_rejected":
+ print("WARNING:")
+ print("WARNING: Project does not accept submit request, request to open a NEW maintenance incident instead")
+ print("WARNING:")
+ xpath = 'maintenance/maintains/@project = \'%s\' and attribute/@name = \'%s\'' % (dst_project, conf.config['maintenance_attribute'])
+ res = search(apiurl, project_id=xpath)
+ root = res['project_id']
+ project = root.find('project')
+ if project is None:
+ print("WARNING: This project is not maintained in the maintenance project specified by '%s', looking elsewhere" % conf.config['maintenance_attribute'])
+ xpath = 'maintenance/maintains/@project = \'%s\'' % dst_project
+ res = search(apiurl, project_id=xpath)
+ root = res['project_id']
+ project = root.find('project')
+ if project is None:
+ raise oscerr.APIError("Server did not define a default maintenance project, can't submit.")
+ tproject = project.get('name')
+ r = create_maintenance_request(apiurl, src_project, [src_package], tproject, dst_project, src_update, message)
+ r = r.reqid
+ else:
+ raise
+
+ return r
+
+
+def get_request(apiurl, reqid):
+ u = makeurl(apiurl, ['request', reqid], {'withfullhistory': '1'})
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+
+ r = Request()
+ r.read(root)
+ return r
+
+
+def change_review_state(apiurl, reqid, newstate, by_user='', by_group='', by_project='', by_package='', message='', supersed=None):
+ query = {'cmd': 'changereviewstate', 'newstate': newstate }
+ if by_user:
+ query['by_user'] = by_user
+ if by_group:
+ query['by_group'] = by_group
+ if by_project:
+ query['by_project'] = by_project
+ if by_package:
+ query['by_package'] = by_package
+ if supersed:
+ query['superseded_by'] = supersed
+ u = makeurl(apiurl, ['request', reqid], query=query)
+ f = http_POST(u, data=message)
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+def change_request_state(apiurl, reqid, newstate, message='', supersed=None, force=False):
+ query = {'cmd': 'changestate', 'newstate': newstate }
+ if supersed:
+ query['superseded_by'] = supersed
+ if force:
+ query['force'] = "1"
+ u = makeurl(apiurl,
+ ['request', reqid], query=query)
+ f = http_POST(u, data=message)
+
+ root = ET.parse(f).getroot()
+ return root.get('code', 'unknown')
+
+def change_request_state_template(req, newstate):
+ if not len(req.actions):
+ return ''
+ action = req.actions[0]
+ tmpl_name = '%srequest_%s_template' % (action.type, newstate)
+ tmpl = conf.config.get(tmpl_name, '')
+ tmpl = tmpl.replace('\\t', '\t').replace('\\n', '\n')
+ data = {'reqid': req.reqid, 'type': action.type, 'who': req.get_creator()}
+ if req.actions[0].type == 'submit':
+ data.update({'src_project': action.src_project,
+ 'src_package': action.src_package, 'src_rev': action.src_rev,
+ 'dst_project': action.tgt_project, 'dst_package': action.tgt_package,
+ 'tgt_project': action.tgt_project, 'tgt_package': action.tgt_package})
+ try:
+ return tmpl % data
+ except KeyError as e:
+ print('error: cannot interpolate \'%s\' in \'%s\'' % (e.args[0], tmpl_name), file=sys.stderr)
+ return ''
+
+def get_review_list(apiurl, project='', package='', byuser='', bygroup='', byproject='', bypackage='', states=()):
+ # this is so ugly...
+ def build_by(xpath, val):
+ if 'all' in states:
+ return xpath_join(xpath, 'review/%s' % val, op='and')
+ elif states:
+ s_xp = ''
+ for state in states:
+ s_xp = xpath_join(s_xp, '@state=\'%s\'' % state, inner=True)
+ val = val.strip('[').strip(']')
+ return xpath_join(xpath, 'review[%s and (%s)]' % (val, s_xp), op='and')
+ else:
+ # default case
+ return xpath_join(xpath, 'review[%s and @state=\'new\']' % val, op='and')
+ return ''
+
+ xpath = ''
+ if states == ():
+ # default: requests which are still open and have reviews in "new" state
+ xpath = xpath_join('', 'state/@name=\'review\'', op='and')
+ xpath = xpath_join(xpath, 'review/@state=\'new\'', op='and')
+ if byuser:
+ xpath = build_by(xpath, '@by_user=\'%s\'' % byuser)
+ if bygroup:
+ xpath = build_by(xpath, '@by_group=\'%s\'' % bygroup)
+ if bypackage:
+ xpath = build_by(xpath, '@by_project=\'%s\' and @by_package=\'%s\'' % (byproject, bypackage))
+ elif byproject:
+ xpath = build_by(xpath, '@by_project=\'%s\'' % byproject)
+
+ # XXX: we cannot use the '|' in the xpath expression because it is not supported
+ # in the backend
+ todo = {}
+ if project:
+ todo['project'] = project
+ if package:
+ todo['package'] = package
+ for kind, val in todo.items():
+ xpath_base = 'action/target/@%(kind)s=\'%(val)s\' or ' \
+ 'submit/target/@%(kind)s=\'%(val)s\''
+
+ if conf.config['include_request_from_project']:
+ xpath_base = xpath_join(xpath_base, 'action/source/@%(kind)s=\'%(val)s\' or ' \
+ 'submit/source/@%(kind)s=\'%(val)s\'', op='or', inner=True)
+ xpath = xpath_join(xpath, xpath_base % {'kind': kind, 'val': val}, op='and', nexpr_parentheses=True)
+
+ if conf.config['verbose'] > 1:
+ print('[ %s ]' % xpath)
+ res = search(apiurl, request=xpath)
+ collection = res['request']
+ requests = []
+ for root in collection.findall('request'):
+ r = Request()
+ r.read(root)
+ requests.append(r)
+ return requests
+
+# this function uses the logic in the api which is faster and more exact then the xpath search
+def get_request_collection(apiurl, role=None, req_who=None, req_states=('new', 'review', 'declined')):
+
+ query={ "view" : "collection" }
+ if role:
+ query["roles"] = role
+ if req_who:
+ query["user"] = req_who
+
+ query["states"] = ",".join(req_states)
+
+ u = makeurl(apiurl, ['request'], query)
+ f = http_GET(u)
+ res = ET.parse(f).getroot()
+
+ requests = []
+ for root in res.findall('request'):
+ r = Request()
+ r.read(root)
+ requests.append(r)
+ return requests
+
+def get_exact_request_list(apiurl, src_project, dst_project, src_package=None, dst_package=None, req_who=None, req_state=('new', 'review', 'declined'), req_type=None):
+ xpath = ''
+ if not 'all' in req_state:
+ for state in req_state:
+ xpath = xpath_join(xpath, 'state/@name=\'%s\'' % state, op='or', inner=True)
+ xpath = '(%s)' % xpath
+ if req_who:
+ xpath = xpath_join(xpath, '(state/@who=\'%(who)s\' or history/@who=\'%(who)s\')' % {'who': req_who}, op='and')
+
+ xpath += " and action[source/@project='%s'" % src_project
+ if src_package:
+ xpath += " and source/@package='%s'" % src_package
+ xpath += " and target/@project='%s'" % dst_project
+ if dst_package:
+ xpath += " and target/@package='%s'" % dst_package
+ xpath += "]"
+ if req_type:
+ xpath += " and action/@type=\'%s\'" % req_type
+
+ if conf.config['verbose'] > 1:
+ print('[ %s ]' % xpath)
+
+ res = search(apiurl, request=xpath)
+ collection = res['request']
+ requests = []
+ for root in collection.findall('request'):
+ r = Request()
+ r.read(root)
+ requests.append(r)
+ return requests
+
+def get_request_list(apiurl, project='', package='', req_who='', req_state=('new', 'review', 'declined'), req_type=None, exclude_target_projects=[]):
+ xpath = ''
+ if not 'all' in req_state:
+ for state in req_state:
+ xpath = xpath_join(xpath, 'state/@name=\'%s\'' % state, inner=True)
+ if req_who:
+ xpath = xpath_join(xpath, '(state/@who=\'%(who)s\' or history/@who=\'%(who)s\')' % {'who': req_who}, op='and')
+
+ # XXX: we cannot use the '|' in the xpath expression because it is not supported
+ # in the backend
+ todo = {}
+ if project:
+ todo['project'] = project
+ if package:
+ todo['package'] = package
+ for kind, val in todo.items():
+ xpath_base = 'action/target/@%(kind)s=\'%(val)s\' or ' \
+ 'submit/target/@%(kind)s=\'%(val)s\''
+
+ if conf.config['include_request_from_project']:
+ xpath_base = xpath_join(xpath_base, 'action/source/@%(kind)s=\'%(val)s\' or ' \
+ 'submit/source/@%(kind)s=\'%(val)s\'', op='or', inner=True)
+ xpath = xpath_join(xpath, xpath_base % {'kind': kind, 'val': val}, op='and', nexpr_parentheses=True)
+
+ if req_type:
+ xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
+ for i in exclude_target_projects:
+ xpath = xpath_join(xpath, '(not(action/target/@project=\'%(prj)s\' or ' \
+ 'submit/target/@project=\'%(prj)s\'))' % {'prj': i}, op='and')
+
+ if conf.config['verbose'] > 1:
+ print('[ %s ]' % xpath)
+ res = search(apiurl, request=xpath)
+ collection = res['request']
+ requests = []
+ for root in collection.findall('request'):
+ r = Request()
+ r.read(root)
+ requests.append(r)
+ return requests
+
+# old style search, this is to be removed
+def get_user_projpkgs_request_list(apiurl, user, req_state=('new', 'review', ), req_type=None, exclude_projects=[], projpkgs={}):
+ """OBSOLETE: user involved request search is supported by OBS 2.2 server side in a better way
+ Return all running requests for all projects/packages where is user is involved"""
+ if not projpkgs:
+ res = get_user_projpkgs(apiurl, user, exclude_projects=exclude_projects)
+ projects = []
+ for i in res['project_id'].findall('project'):
+ projpkgs[i.get('name')] = []
+ projects.append(i.get('name'))
+ for i in res['package_id'].findall('package'):
+ if not i.get('project') in projects:
+ projpkgs.setdefault(i.get('project'), []).append(i.get('name'))
+ xpath = ''
+ for prj, pacs in projpkgs.items():
+ if not len(pacs):
+ xpath = xpath_join(xpath, 'action/target/@project=\'%s\'' % prj, inner=True)
+ else:
+ xp = ''
+ for p in pacs:
+ xp = xpath_join(xp, 'action/target/@package=\'%s\'' % p, inner=True)
+ xp = xpath_join(xp, 'action/target/@project=\'%s\'' % prj, op='and')
+ xpath = xpath_join(xpath, xp, inner=True)
+ if req_type:
+ xpath = xpath_join(xpath, 'action/@type=\'%s\'' % req_type, op='and')
+ if not 'all' in req_state:
+ xp = ''
+ for state in req_state:
+ xp = xpath_join(xp, 'state/@name=\'%s\'' % state, inner=True)
+ xpath = xpath_join(xp, xpath, op='and', nexpr_parentheses=True)
+ res = search(apiurl, request=xpath)
+ result = []
+ for root in res['request'].findall('request'):
+ r = Request()
+ r.read(root)
+ result.append(r)
+ return result
+
+def get_request_log(apiurl, reqid):
+ r = get_request(apiurl, reqid)
+ data = []
+ frmt = '-' * 76 + '\n%s | %s | %s\n\n%s'
+ r.statehistory.reverse()
+ # the description of the request is used for the initial log entry
+ # otherwise its comment attribute would contain None
+ if len(r.statehistory) >= 1:
+ r.statehistory[-1].comment = r.description
+ else:
+ r.state.comment = r.description
+ for state in [ r.state ] + r.statehistory:
+ s = frmt % (state.name, state.who, state.when, str(state.comment))
+ data.append(s)
+ return data
+
+def check_existing_requests(apiurl, src_project, src_package, dst_project,
+ dst_package):
+ reqs = get_exact_request_list(apiurl, src_project, dst_project,
+ src_package, dst_package,
+ req_type='submit',
+ req_state=['new', 'review', 'declined'])
+ repl = ''
+ if reqs:
+ print('There are already the following submit request: %s.' % \
+ ', '.join([i.reqid for i in reqs]))
+ repl = raw_input('Supersede the old requests? (y/n/c) ')
+ if repl.lower() == 'c':
+ print('Aborting', file=sys.stderr)
+ raise oscerr.UserAbort()
+ return repl == 'y', reqs
+
+def check_existing_maintenance_requests(apiurl, src_project, src_packages, dst_project,
+ release_project):
+ reqs = []
+ for src_package in src_packages:
+ reqs += get_exact_request_list(apiurl, src_project, dst_project,
+ src_package, None,
+ req_type='maintenance_incident',
+ req_state=['new', 'review', 'declined'])
+ repl = ''
+ if reqs:
+ print('There are already the following maintenance incident request: %s.' % \
+ ', '.join([i.reqid for i in reqs]))
+ repl = raw_input('Supersede the old requests? (y/n/c) ')
+ if repl.lower() == 'c':
+ print('Aborting', file=sys.stderr)
+ raise oscerr.UserAbort()
+ return repl == 'y', reqs
+
+def get_group(apiurl, group):
+ u = makeurl(apiurl, ['group', quote_plus(group)])
+ try:
+ f = http_GET(u)
+ return ''.join(f.readlines())
+ except HTTPError:
+ print('group \'%s\' not found' % group)
+ return None
+
+def get_user_meta(apiurl, user):
+ u = makeurl(apiurl, ['person', quote_plus(user)])
+ try:
+ f = http_GET(u)
+ return ''.join(f.readlines())
+ except HTTPError:
+ print('user \'%s\' not found' % user)
+ return None
+
+
+def _get_xml_data(meta, *tags):
+ data = []
+ if meta != None:
+ root = ET.fromstring(meta)
+ for tag in tags:
+ elm = root.find(tag)
+ if elm is None or elm.text is None:
+ data.append('-')
+ else:
+ data.append(elm.text)
+ return data
+
+
+def get_user_data(apiurl, user, *tags):
+ """get specified tags from the user meta"""
+ meta = get_user_meta(apiurl, user)
+ return _get_xml_data(meta, *tags)
+
+
+def get_group_data(apiurl, group, *tags):
+ meta = get_group(apiurl, group)
+ return _get_xml_data(meta, *tags)
+
+
+def download(url, filename, progress_obj = None, mtime = None):
+ import tempfile, shutil
+ global BUFSIZE
+
+ o = None
+ try:
+ prefix = os.path.basename(filename)
+ path = os.path.dirname(filename)
+ (fd, tmpfile) = tempfile.mkstemp(dir=path, prefix = prefix, suffix = '.osctmp')
+ os.fchmod(fd, 0o644)
+ try:
+ o = os.fdopen(fd, 'wb')
+ for buf in streamfile(url, http_GET, BUFSIZE, progress_obj=progress_obj):
+ o.write(bytes(buf, "utf-8"))
+ o.close()
+ os.rename(tmpfile, filename)
+ except:
+ os.unlink(tmpfile)
+ raise
+ finally:
+ if o is not None:
+ o.close()
+
+ if mtime:
+ utime(filename, (-1, mtime))
+
+def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision=None, progress_obj=None, mtime=None, meta=False):
+ targetfilename = targetfilename or filename
+ query = {}
+ if meta:
+ query['rev'] = 1
+ if revision:
+ query['rev'] = revision
+ u = makeurl(apiurl, ['source', prj, package, pathname2url(filename.encode(locale.getpreferredencoding(), 'replace'))], query=query)
+ download(u, targetfilename, progress_obj, mtime)
+
+def get_binary_file(apiurl, prj, repo, arch,
+ filename,
+ package = None,
+ target_filename = None,
+ target_mtime = None,
+ progress_meter = False):
+ progress_obj = None
+ if progress_meter:
+ from .meter import TextMeter
+ progress_obj = TextMeter()
+
+ target_filename = target_filename or filename
+
+ where = package or '_repository'
+ u = makeurl(apiurl, ['build', prj, repo, arch, where, filename])
+ download(u, target_filename, progress_obj, target_mtime)
+
+def dgst_from_string(str):
+ # Python 2.5 depracates the md5 modules
+ # Python 2.4 doesn't have hashlib yet
+ try:
+ import hashlib
+ md5_hash = hashlib.md5()
+ except ImportError:
+ import md5
+ md5_hash = md5.new()
+ md5_hash.update(str)
+ return md5_hash.hexdigest()
+
+def dgst(file):
+
+ #if not os.path.exists(file):
+ #return None
+
+ global BUFSIZE
+
+ try:
+ import hashlib
+ md5 = hashlib
+ except ImportError:
+ import md5
+ md5 = md5
+ s = md5.md5()
+ f = open(file, 'rb')
+ while True:
+ buf = f.read(BUFSIZE)
+ if not buf: break
+ s.update(buf)
+ return s.hexdigest()
+ f.close()
+
+
+def binary(s):
+ """return true if a string is binary data using diff's heuristic"""
+ if s and bytes('\0', "utf-8") in s[:4096]:
+ return True
+ return False
+
+
+def binary_file(fn):
+ """read 4096 bytes from a file named fn, and call binary() on the data"""
+ return binary(open(fn, 'rb').read(4096))
+
+
+def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None):
+ """
+ This methods diffs oldfilename against filename (so filename will
+ be shown as the new file).
+ The variable origfilename is used if filename and oldfilename differ
+ in their names (for instance if a tempfile is used for filename etc.)
+ """
+
+ import difflib
+
+ global store
+
+ if not oldfilename:
+ oldfilename = filename
+
+ if not olddir:
+ olddir = os.path.join(dir, store)
+
+ if not origfilename:
+ origfilename = filename
+
+ file1 = os.path.join(olddir, oldfilename) # old/stored original
+ file2 = os.path.join(dir, filename) # working copy
+ if binary_file(file1) or binary_file(file2):
+ return ['Binary file \'%s\' has changed.\n' % origfilename]
+
+ f1 = f2 = None
+ try:
+ f1 = open(file1, 'rt')
+ s1 = f1.readlines()
+ f1.close()
+
+ f2 = open(file2, 'rt')
+ s2 = f2.readlines()
+ f2.close()
+ finally:
+ if f1:
+ f1.close()
+ if f2:
+ f2.close()
+
+ d = difflib.unified_diff(s1, s2,
+ fromfile = '%s\t(revision %s)' % (origfilename, rev), \
+ tofile = '%s\t(working copy)' % origfilename)
+ d = list(d)
+ # python2.7's difflib slightly changed the format
+ # adapt old format to the new format
+ if len(d) > 1:
+ d[0] = d[0].replace(' \n', '\n')
+ d[1] = d[1].replace(' \n', '\n')
+
+ # if file doesn't end with newline, we need to append one in the diff result
+ for i, line in enumerate(d):
+ if not line.endswith('\n'):
+ d[i] += '\n\\ No newline at end of file'
+ if i+1 != len(d):
+ d[i] += '\n'
+ return d
+
+def server_diff(apiurl,
+ old_project, old_package, old_revision,
+ new_project, new_package, new_revision,
+ unified=False, missingok=False, meta=False, expand=True, full=True):
+ query = {'cmd': 'diff'}
+ if expand:
+ query['expand'] = 1
+ if old_project:
+ query['oproject'] = old_project
+ if old_package:
+ query['opackage'] = old_package
+ if old_revision:
+ query['orev'] = old_revision
+ if new_revision:
+ query['rev'] = new_revision
+ if unified:
+ query['unified'] = 1
+ if missingok:
+ query['missingok'] = 1
+ if meta:
+ query['meta'] = 1
+ if full:
+ query['filelimit'] = 0
+ query['tarlimit'] = 0
+
+ u = makeurl(apiurl, ['source', new_project, new_package], query=query)
+
+ f = http_POST(u)
+ return f.read()
+
+def server_diff_noex(apiurl,
+ old_project, old_package, old_revision,
+ new_project, new_package, new_revision,
+ unified=False, missingok=False, meta=False, expand=True):
+ try:
+ return server_diff(apiurl,
+ old_project, old_package, old_revision,
+ new_project, new_package, new_revision,
+ unified, missingok, meta, expand)
+ except HTTPError as e:
+ msg = None
+ body = None
+ try:
+ body = e.read()
+ if not 'bad link' in body:
+ return '# diff failed: ' + body
+ except:
+ return '# diff failed with unknown error'
+
+ if expand:
+ rdiff = "## diff on expanded link not possible, showing unexpanded version\n"
+ try:
+ rdiff += server_diff_noex(apiurl,
+ old_project, old_package, old_revision,
+ new_project, new_package, new_revision,
+ unified, missingok, meta, False)
+ except:
+ elm = ET.fromstring(body).find('summary')
+ summary = ''
+ if not elm is None:
+ summary = elm.text
+ return 'error: diffing failed: %s' % summary
+ return rdiff
+
+
+def request_diff(apiurl, reqid):
+ u = makeurl(apiurl, ['request', reqid], query={'cmd': 'diff'} )
+
+ f = http_POST(u)
+ return f.read()
+
+def submit_action_diff(apiurl, action):
+ """diff a single submit action"""
+ # backward compatiblity: only a recent api/backend supports the missingok parameter
+ try:
+ return server_diff(apiurl, action.tgt_project, action.tgt_package, None,
+ action.src_project, action.src_package, action.src_rev, True, True)
+ except HTTPError as e:
+ if e.code == 400:
+ try:
+ return server_diff(apiurl, action.tgt_project, action.tgt_package, None,
+ action.src_project, action.src_package, action.src_rev, True, False)
+ except HTTPError as e:
+ if e.code != 404:
+ raise e
+ root = ET.fromstring(e.read())
+ return 'error: \'%s\' does not exist' % root.find('summary').text
+ elif e.code == 404:
+ root = ET.fromstring(e.read())
+ return 'error: \'%s\' does not exist' % root.find('summary').text
+ raise e
+
+def make_dir(apiurl, project, package, pathname=None, prj_dir=None, package_tracking=True, pkg_path=None):
+ """
+ creates the plain directory structure for a package dir.
+ The 'apiurl' parameter is needed for the project dir initialization.
+ The 'project' and 'package' parameters specify the name of the
+ project and the package. The optional 'pathname' parameter is used
+ for printing out the message that a new dir was created (default: 'prj_dir/package').
+ The optional 'prj_dir' parameter specifies the path to the project dir (default: 'project').
+ If pkg_path is not None store the package's content in pkg_path (no project structure is created)
+ """
+ prj_dir = prj_dir or project
+
+ # FIXME: carefully test each patch component of prj_dir,
+ # if we have a .osc/_files entry at that level.
+ # -> if so, we have a package/project clash,
+ # and should rename this path component by appending '.proj'
+ # and give user a warning message, to discourage such clashes
+
+ if pkg_path is None:
+ pathname = pathname or getTransActPath(os.path.join(prj_dir, package))
+ pkg_path = os.path.join(prj_dir, package)
+ if is_package_dir(prj_dir):
+ # we want this to become a project directory,
+ # but it already is a package directory.
+ raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving myself away not implemented')
+
+ if not is_project_dir(prj_dir):
+ # this directory could exist as a parent direory for one of our earlier
+ # checked out sub-projects. in this case, we still need to initialize it.
+ print(statfrmt('A', prj_dir))
+ Project.init_project(apiurl, prj_dir, project, package_tracking)
+
+ if is_project_dir(os.path.join(prj_dir, package)):
+ # the thing exists, but is a project directory and not a package directory
+ # FIXME: this should be a warning message to discourage package/project clashes
+ raise oscerr.OscIOError(None, 'checkout_package: package/project clash. Moving project away not implemented')
+ else:
+ pathname = pkg_path
+
+ if not os.path.exists(pkg_path):
+ print(statfrmt('A', pathname))
+ os.mkdir(os.path.join(pkg_path))
+# os.mkdir(os.path.join(prj_dir, package, store))
+
+ return pkg_path
+
+
+def checkout_package(apiurl, project, package,
+ revision=None, pathname=None, prj_obj=None,
+ expand_link=False, prj_dir=None, server_service_files = None, service_files=None, progress_obj=None, size_limit=None, meta=False, outdir=None):
+ try:
+ # the project we're in might be deleted.
+ # that'll throw an error then.
+ olddir = os.getcwd()
+ except:
+ olddir = os.environ.get("PWD")
+
+ if not prj_dir:
+ prj_dir = olddir
+ else:
+ if sys.platform[:3] == 'win':
+ prj_dir = prj_dir[:2] + prj_dir[2:].replace(':', ';')
+ else:
+ if conf.config['checkout_no_colon']:
+ prj_dir = prj_dir.replace(':', '/')
+
+ root_dots = '.'
+ if conf.config['checkout_rooted']:
+ if prj_dir[:1] == '/':
+ if conf.config['verbose'] > 1:
+ print("checkout_rooted ignored for %s" % prj_dir)
+ # ?? should we complain if not is_project_dir(prj_dir) ??
+ else:
+ # if we are inside a project or package dir, ascend to parent
+ # directories, so that all projects are checked out relative to
+ # the same root.
+ if is_project_dir(".."):
+ # if we are in a package dir, goto parent.
+ # Hmm, with 'checkout_no_colon' in effect, we have directory levels that
+ # do not easily reveal the fact, that they are part of a project path.
+ # At least this test should find that the parent of 'home/username/branches'
+ # is a project (hack alert). Also goto parent in this case.
+ root_dots = "../"
+ elif is_project_dir("../.."):
+ # testing two levels is better than one.
+ # May happen in case of checkout_no_colon, or
+ # if project roots were previously inconsistent
+ root_dots = "../../"
+ if is_project_dir(root_dots):
+ if conf.config['checkout_no_colon']:
+ oldproj = store_read_project(root_dots)
+ n = len(oldproj.split(':'))
+ else:
+ n = 1
+ root_dots = root_dots + "../" * n
+
+ if root_dots != '.':
+ if conf.config['verbose']:
+ print("found root of %s at %s" % (oldproj, root_dots))
+ prj_dir = root_dots + prj_dir
+
+ if not pathname:
+ pathname = getTransActPath(os.path.join(prj_dir, package))
+
+ # before we create directories and stuff, check if the package actually
+ # exists
+ show_package_meta(apiurl, project, package, meta)
+
+ isfrozen = False
+ if expand_link:
+ # try to read from the linkinfo
+ # if it is a link we use the xsrcmd5 as the revision to be
+ # checked out
+ try:
+ x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision, meta=meta, include_service_files=server_service_files)
+ except:
+ x = show_upstream_xsrcmd5(apiurl, project, package, revision=revision, meta=meta, linkrev='base', include_service_files=server_service_files)
+ if x:
+ isfrozen = True
+ if x:
+ revision = x
+ directory = make_dir(apiurl, project, package, pathname, prj_dir, conf.config['do_package_tracking'], outdir)
+ p = Package.init_package(apiurl, project, package, directory, size_limit, meta, progress_obj)
+ if isfrozen:
+ p.mark_frozen()
+ # no project structure is wanted when outdir is used
+ if conf.config['do_package_tracking'] and outdir is None:
+ # check if we can re-use an existing project object
+ if prj_obj is None:
+ prj_obj = Project(prj_dir)
+ prj_obj.set_state(p.name, ' ')
+ prj_obj.write_packages()
+ p.update(revision, server_service_files, size_limit)
+ if service_files:
+ print('Running all source services local')
+ p.run_source_services()
+
+def replace_pkg_meta(pkgmeta, new_name, new_prj, keep_maintainers = False,
+ dst_userid = None, keep_develproject = False):
+ """
+ update pkgmeta with new new_name and new_prj and set calling user as the
+ only maintainer (unless keep_maintainers is set). Additionally remove the
+ develproject entry (<devel />) unless keep_develproject is true.
+ """
+ root = ET.fromstring(''.join(pkgmeta))
+ root.set('name', new_name)
+ root.set('project', new_prj)
+ if not keep_maintainers:
+ for person in root.findall('person'):
+ root.remove(person)
+ if not keep_develproject:
+ for dp in root.findall('devel'):
+ root.remove(dp)
+ return ET.tostring(root, encoding=ET_ENCODING)
+
+def link_to_branch(apiurl, project, package):
+ """
+ convert a package with a _link + project.diff to a branch
+ """
+
+ if '_link' in meta_get_filelist(apiurl, project, package):
+ u = makeurl(apiurl, ['source', project, package], 'cmd=linktobranch')
+ http_POST(u)
+ else:
+ raise oscerr.OscIOError(None, 'no _link file inside project \'%s\' package \'%s\'' % (project, package))
+
+def link_pac(src_project, src_package, dst_project, dst_package, force, rev='', cicount='', disable_publish = False, missing_target = False, vrev=''):
+ """
+ create a linked package
+ - "src" is the original package
+ - "dst" is the "link" package that we are creating here
+ """
+ meta_change = False
+ dst_meta = ''
+ apiurl = conf.config['apiurl']
+ try:
+ dst_meta = meta_exists(metatype='pkg',
+ path_args=(quote_plus(dst_project), quote_plus(dst_package)),
+ template_args=None,
+ create_new=False, apiurl=apiurl)
+ root = ET.fromstring(''.join(dst_meta))
+ if root.get('project') != dst_project:
+ # The source comes from a different project via a project link, we need to create this instance
+ meta_change = True
+ except:
+ meta_change = True
+
+ if meta_change:
+ if missing_target:
+ dst_meta = '<package name="%s"><title/><description/></package>' % dst_package
+ else:
+ src_meta = show_package_meta(apiurl, src_project, src_package)
+ dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
+
+ if disable_publish:
+ meta_change = True
+ root = ET.fromstring(''.join(dst_meta))
+ elm = root.find('publish')
+ if not elm:
+ elm = ET.SubElement(root, 'publish')
+ elm.clear()
+ ET.SubElement(elm, 'disable')
+ dst_meta = ET.tostring(root, encoding=ET_ENCODING)
+
+ if meta_change:
+ edit_meta('pkg',
+ path_args=(dst_project, dst_package),
+ data=dst_meta)
+ # create the _link file
+ # but first, make sure not to overwrite an existing one
+ if '_link' in meta_get_filelist(apiurl, dst_project, dst_package):
+ if force:
+ print('forced overwrite of existing _link file', file=sys.stderr)
+ else:
+ print(file=sys.stderr)
+ print('_link file already exists...! Aborting', file=sys.stderr)
+ sys.exit(1)
+
+ if rev:
+ rev = ' rev="%s"' % rev
+ else:
+ rev = ''
+
+ if vrev:
+ vrev = ' vrev="%s"' % vrev
+ else:
+ vrev = ''
+
+ missingok = ''
+ if missing_target:
+ missingok = ' missingok="true"'
+
+ if cicount:
+ cicount = ' cicount="%s"' % cicount
+ else:
+ cicount = ''
+
+ print('Creating _link...', end=' ')
+
+ project = ''
+ if src_project != dst_project:
+ project = 'project="%s"' % src_project
+
+ link_template = """\
+<link %s package="%s"%s%s%s%s>
+<patches>
+ <!-- <branch /> for a full copy, default case -->
+ <!-- <apply name="patch" /> apply a patch on the source directory -->
+ <!-- <topadd>%%define build_with_feature_x 1</topadd> add a line on the top (spec file only) -->
+ <!-- <add name="file.patch" /> add a patch to be applied after %%setup (spec file only) -->
+ <!-- <delete name="filename" /> delete a file -->
+</patches>
+</link>
+""" % (project, src_package, missingok, rev, vrev, cicount)
+
+ u = makeurl(apiurl, ['source', dst_project, dst_package, '_link'])
+ http_PUT(u, data=link_template)
+ print('Done.')
+
+def aggregate_pac(src_project, src_package, dst_project, dst_package, repo_map = {}, disable_publish = False, nosources = False):
+ """
+ aggregate package
+ - "src" is the original package
+ - "dst" is the "aggregate" package that we are creating here
+ - "map" is a dictionary SRC => TARGET repository mappings
+ """
+ meta_change = False
+ dst_meta = ''
+ apiurl = conf.config['apiurl']
+ try:
+ dst_meta = meta_exists(metatype='pkg',
+ path_args=(quote_plus(dst_project), quote_plus(dst_package)),
+ template_args=None,
+ create_new=False, apiurl=apiurl)
+ root = ET.fromstring(''.join(dst_meta))
+ if root.get('project') != dst_project:
+ # The source comes from a different project via a project link, we need to create this instance
+ meta_change = True
+ except:
+ meta_change = True
+
+ if meta_change:
+ src_meta = show_package_meta(apiurl, src_project, src_package)
+ dst_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
+ meta_change = True
+
+ if disable_publish:
+ meta_change = True
+ root = ET.fromstring(''.join(dst_meta))
+ elm = root.find('publish')
+ if not elm:
+ elm = ET.SubElement(root, 'publish')
+ elm.clear()
+ ET.SubElement(elm, 'disable')
+ dst_meta = ET.tostring(root, encoding=ET_ENCODING)
+ if meta_change:
+ edit_meta('pkg',
+ path_args=(dst_project, dst_package),
+ data=dst_meta)
+
+ # create the _aggregate file
+ # but first, make sure not to overwrite an existing one
+ if '_aggregate' in meta_get_filelist(apiurl, dst_project, dst_package):
+ print(file=sys.stderr)
+ print('_aggregate file already exists...! Aborting', file=sys.stderr)
+ sys.exit(1)
+
+ print('Creating _aggregate...', end=' ')
+ aggregate_template = """\
+<aggregatelist>
+ <aggregate project="%s">
+""" % (src_project)
+
+ aggregate_template += """\
+ <package>%s</package>
+""" % ( src_package)
+
+ if nosources:
+ aggregate_template += """\
+ <nosources />
+"""
+ for src, tgt in repo_map.items():
+ aggregate_template += """\
+ <repository target="%s" source="%s" />
+""" % (tgt, src)
+
+ aggregate_template += """\
+ </aggregate>
+</aggregatelist>
+"""
+
+ u = makeurl(apiurl, ['source', dst_project, dst_package, '_aggregate'])
+ http_PUT(u, data=aggregate_template)
+ print('Done.')
+
+
+def attribute_branch_pkg(apiurl, attribute, maintained_update_project_attribute, package, targetproject, return_existing=False, force=False, noaccess=False, add_repositories=False, dryrun=False, nodevelproject=False, maintenance=False):
+ """
+ Branch packages defined via attributes (via API call)
+ """
+ query = { 'cmd': 'branch' }
+ query['attribute'] = attribute
+ if targetproject:
+ query['target_project'] = targetproject
+ if dryrun:
+ query['dryrun'] = "1"
+ if force:
+ query['force'] = "1"
+ if noaccess:
+ query['noaccess'] = "1"
+ if nodevelproject:
+ query['ignoredevel'] = '1'
+ if add_repositories:
+ query['add_repositories'] = "1"
+ if maintenance:
+ query['maintenance'] = "1"
+ if package:
+ query['package'] = package
+ if maintained_update_project_attribute:
+ query['update_project_attribute'] = maintained_update_project_attribute
+
+ u = makeurl(apiurl, ['source'], query=query)
+ f = None
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ msg = ''.join(e.readlines())
+ msg = msg.split('<summary>')[1]
+ msg = msg.split('</summary>')[0]
+ raise oscerr.APIError(msg)
+
+ r = None
+
+ root = ET.fromstring(f.read())
+ if dryrun:
+ return root
+ # TODO: change api here and return parsed XML as class
+ if conf.config['http_debug']:
+ print(ET.tostring(root, encoding=ET_ENCODING), file=sys.stderr)
+ for node in root.findall('data'):
+ r = node.get('name')
+ if r and r == 'targetproject':
+ return node.text
+
+ return r
+
+
+def branch_pkg(apiurl, src_project, src_package, nodevelproject=False, rev=None, linkrev=None, target_project=None, target_package=None, return_existing=False, msg='', force=False, noaccess=False, add_repositories=False, add_repositories_block=None, add_repositories_rebuild=None, extend_package_names=False, missingok=False, maintenance=False, newinstance=False):
+ """
+ Branch a package (via API call)
+ """
+ query = { 'cmd': 'branch' }
+ if nodevelproject:
+ query['ignoredevel'] = '1'
+ if force:
+ query['force'] = '1'
+ if noaccess:
+ query['noaccess'] = '1'
+ if add_repositories:
+ query['add_repositories'] = "1"
+ if add_repositories_block:
+ query['add_repositories_block'] = add_repositories_block
+ if add_repositories_rebuild:
+ query['add_repositories_rebuild'] = add_repositories_rebuild
+ if maintenance:
+ query['maintenance'] = "1"
+ if missingok:
+ query['missingok'] = "1"
+ if newinstance:
+ query['newinstance'] = "1"
+ if extend_package_names:
+ query['extend_package_names'] = "1"
+ if rev:
+ query['rev'] = rev
+ if linkrev:
+ query['linkrev'] = linkrev
+ if target_project:
+ query['target_project'] = target_project
+ if target_package:
+ query['target_package'] = target_package
+ if msg:
+ query['comment'] = msg
+ u = makeurl(apiurl, ['source', src_project, src_package], query=query)
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ root = ET.fromstring(e.read())
+ if missingok:
+ if root and root.get('code') == "not_missing":
+ raise oscerr.NotMissing("Package exists already via project link, but link will point to given project")
+ summary = root.find('summary')
+ if summary is None:
+ raise oscerr.APIError('unexpected response:\n%s' % ET.tostring(root, encoding=ET_ENCODING))
+ if not return_existing:
+ raise oscerr.APIError('failed to branch: %s' % summary.text)
+ m = re.match(r"branch target package already exists: (\S+)/(\S+)", summary.text)
+ if not m:
+ e.msg += '\n' + summary.text
+ raise
+ return (True, m.group(1), m.group(2), None, None)
+
+ root = ET.fromstring(f.read())
+ if conf.config['http_debug']:
+ print(ET.tostring(root, encoding=ET_ENCODING), file=sys.stderr)
+ data = {}
+ for i in root.findall('data'):
+ data[i.get('name')] = i.text
+ return (False, data.get('targetproject', None), data.get('targetpackage', None),
+ data.get('sourceproject', None), data.get('sourcepackage', None))
+
+
+def copy_pac(src_apiurl, src_project, src_package,
+ dst_apiurl, dst_project, dst_package,
+ client_side_copy = False,
+ keep_maintainers = False,
+ keep_develproject = False,
+ expand = False,
+ revision = None,
+ comment = None,
+ force_meta_update = None,
+ keep_link = None):
+ """
+ Create a copy of a package.
+
+ Copying can be done by downloading the files from one package and commit
+ them into the other by uploading them (client-side copy) --
+ or by the server, in a single api call.
+ """
+
+ if not (src_apiurl == dst_apiurl and src_project == dst_project \
+ and src_package == dst_package):
+ src_meta = show_package_meta(src_apiurl, src_project, src_package)
+ dst_userid = conf.get_apiurl_usr(dst_apiurl)
+ src_meta = replace_pkg_meta(src_meta, dst_package, dst_project, keep_maintainers,
+ dst_userid, keep_develproject)
+
+ url = make_meta_url('pkg', (quote_plus(dst_project),) + (quote_plus(dst_package),), dst_apiurl)
+ found = None
+ try:
+ found = http_GET(url).readlines()
+ except HTTPError as e:
+ pass
+ if force_meta_update or not found:
+ print('Sending meta data...')
+ u = makeurl(dst_apiurl, ['source', dst_project, dst_package, '_meta'])
+ http_PUT(u, data=src_meta)
+
+ print('Copying files...')
+ if not client_side_copy:
+ query = {'cmd': 'copy', 'oproject': src_project, 'opackage': src_package }
+ if expand or keep_link:
+ query['expand'] = '1'
+ if keep_link:
+ query['keeplink'] = '1'
+ if revision:
+ query['orev'] = revision
+ if comment:
+ query['comment'] = comment
+ u = makeurl(dst_apiurl, ['source', dst_project, dst_package], query=query)
+ f = http_POST(u)
+ return f.read()
+
+ else:
+ # copy one file after the other
+ import tempfile
+ query = {'rev': 'upload'}
+ xml = show_files_meta(src_apiurl, src_project, src_package,
+ expand=expand, revision=revision)
+ filelist = ET.fromstring(xml)
+ revision = filelist.get('srcmd5')
+ # filter out _service: files
+ for entry in filelist.findall('entry'):
+ # hmm the old code also checked for _service_ (but this is
+ # probably a relict from former times (if at all))
+ if entry.get('name').startswith('_service:'):
+ filelist.remove(entry)
+ tfilelist = Package.commit_filelist(dst_apiurl, dst_project,
+ dst_package, filelist, msg=comment)
+ todo = Package.commit_get_missing(tfilelist)
+ for filename in todo:
+ print(' ', filename)
+ # hmm ideally, we would pass a file-like (that delegates to
+ # streamfile) to http_PUT...
+ with tempfile.NamedTemporaryFile(prefix='osc-copypac') as f:
+ get_source_file(src_apiurl, src_project, src_package, filename,
+ targetfilename=f.name, revision=revision)
+ path = ['source', dst_project, dst_package, pathname2url(filename)]
+ u = makeurl(dst_apiurl, path, query={'rev': 'repository'})
+ http_PUT(u, file=f.name)
+ tfilelist = Package.commit_filelist(dst_apiurl, dst_project, dst_package,
+ filelist, msg=comment)
+ todo = Package.commit_get_missing(tfilelist)
+ if todo:
+ raise oscerr.APIError('failed to copy: %s' % ', '.join(todo))
+ return 'Done.'
+
+
+def unlock_package(apiurl, prj, pac, msg):
+ query = {'cmd': 'unlock', 'comment': msg}
+ u = makeurl(apiurl, ['source', prj, pac], query)
+ http_POST(u)
+
+def unlock_project(apiurl, prj, msg=None):
+ query = {'cmd': 'unlock', 'comment': msg}
+ u = makeurl(apiurl, ['source', prj], query)
+ http_POST(u)
+
+
+def undelete_package(apiurl, prj, pac, msg=None):
+ query = {'cmd': 'undelete'}
+ if msg:
+ query['comment'] = msg
+ else:
+ query['comment'] = 'undeleted via osc'
+ u = makeurl(apiurl, ['source', prj, pac], query)
+ http_POST(u)
+
+def undelete_project(apiurl, prj, msg=None):
+ query = {'cmd': 'undelete'}
+ if msg:
+ query['comment'] = msg
+ else:
+ query['comment'] = 'undeleted via osc'
+ u = makeurl(apiurl, ['source', prj], query)
+ http_POST(u)
+
+
+def delete_package(apiurl, prj, pac, force=False, msg=None):
+ query = {}
+ if force:
+ query['force'] = "1"
+ if msg:
+ query['comment'] = msg
+ u = makeurl(apiurl, ['source', prj, pac], query)
+ http_DELETE(u)
+
+def delete_project(apiurl, prj, force=False, msg=None):
+ query = {}
+ if force:
+ query['force'] = "1"
+ if msg:
+ query['comment'] = msg
+ u = makeurl(apiurl, ['source', prj], query)
+ http_DELETE(u)
+
+def delete_files(apiurl, prj, pac, files):
+ for filename in files:
+ u = makeurl(apiurl, ['source', prj, pac, filename], query={'comment': 'removed %s' % (filename, )})
+ http_DELETE(u)
+
+
+# old compat lib call
+def get_platforms(apiurl):
+ return get_repositories(apiurl)
+
+def get_repositories(apiurl):
+ f = http_GET(makeurl(apiurl, ['platform']))
+ tree = ET.parse(f)
+ r = sorted([ node.get('name') for node in tree.getroot() ])
+ return r
+
+
+def get_distibutions(apiurl, discon=False):
+ r = []
+
+ # FIXME: this is just a naming convention on api.opensuse.org, but not a general valid apparoach
+ if discon:
+ result_line_templ = '%(name)-25s %(project)s'
+ f = http_GET(makeurl(apiurl, ['build']))
+ root = ET.fromstring(''.join(f))
+
+ for node in root.findall('entry'):
+ if node.get('name').startswith('DISCONTINUED:'):
+ rmap = {}
+ rmap['name'] = node.get('name').replace('DISCONTINUED:', '').replace(':', ' ')
+ rmap['project'] = node.get('name')
+ r.append (result_line_templ % rmap)
+
+ r.insert(0, 'distribution project')
+ r.insert(1, '------------ -------')
+
+ else:
+ result_line_templ = '%(name)-25s %(project)-25s %(repository)-25s %(reponame)s'
+ f = http_GET(makeurl(apiurl, ['distributions']))
+ root = ET.fromstring(''.join(f))
+
+ for node in root.findall('distribution'):
+ rmap = {}
+ for node2 in node.findall('name'):
+ rmap['name'] = node2.text
+ for node3 in node.findall('project'):
+ rmap['project'] = node3.text
+ for node4 in node.findall('repository'):
+ rmap['repository'] = node4.text
+ for node5 in node.findall('reponame'):
+ rmap['reponame'] = node5.text
+ r.append(result_line_templ % rmap)
+
+ r.insert(0, 'distribution project repository reponame')
+ r.insert(1, '------------ ------- ---------- --------')
+
+ return r
+
+
+# old compat lib call
+def get_platforms_of_project(apiurl, prj):
+ return get_repositories_of_project(apiurl, prj)
+
+def get_repositories_of_project(apiurl, prj):
+ f = show_project_meta(apiurl, prj)
+ root = ET.fromstring(''.join(f))
+
+ r = [ node.get('name') for node in root.findall('repository')]
+ return r
+
+
+class Repo:
+ repo_line_templ = '%-15s %-10s'
+
+ def __init__(self, name, arch):
+ self.name = name
+ self.arch = arch
+
+ def __str__(self):
+ return self.repo_line_templ % (self.name, self.arch)
+
+ def __repr__(self):
+ return 'Repo(%s %s)' % (self.name, self.arch)
+
+ @staticmethod
+ def fromfile(filename):
+ if not os.path.exists(filename):
+ return []
+ repos = []
+ lines = open(filename, 'r').readlines()
+ for line in lines:
+ data = line.split()
+ if len(data) == 2:
+ repos.append(Repo(data[0], data[1]))
+ elif len(data) == 1:
+ # only for backward compatibility
+ repos.append(Repo(data[0], ''))
+ return repos
+
+ @staticmethod
+ def tofile(filename, repos):
+ with open(filename, 'w') as f:
+ for repo in repos:
+ f.write('%s %s\n' % (repo.name, repo.arch))
+
+def get_repos_of_project(apiurl, prj):
+ f = show_project_meta(apiurl, prj)
+ root = ET.fromstring(''.join(f))
+
+ for node in root.findall('repository'):
+ for node2 in node.findall('arch'):
+ yield Repo(node.get('name'), node2.text)
+
+def get_binarylist(apiurl, prj, repo, arch, package=None, verbose=False):
+ what = package or '_repository'
+ u = makeurl(apiurl, ['build', prj, repo, arch, what])
+ f = http_GET(u)
+ tree = ET.parse(f)
+ if not verbose:
+ return [ node.get('filename') for node in tree.findall('binary')]
+ else:
+ l = []
+ for node in tree.findall('binary'):
+ f = File(node.get('filename'),
+ None,
+ int(node.get('size')),
+ int(node.get('mtime')))
+ l.append(f)
+ return l
+
+
+def get_binarylist_published(apiurl, prj, repo, arch):
+ u = makeurl(apiurl, ['published', prj, repo, arch])
+ f = http_GET(u)
+ tree = ET.parse(f)
+ r = [ node.get('name') for node in tree.findall('entry')]
+ return r
+
+
+def show_results_meta(apiurl, prj, package=None, lastbuild=None, repository=[], arch=[], oldstate=None):
+ query = {}
+ if package:
+ query['package'] = package
+ if oldstate:
+ query['oldstate'] = oldstate
+ if lastbuild:
+ query['lastbuild'] = 1
+ u = makeurl(apiurl, ['build', prj, '_result'], query=query)
+ for repo in repository:
+ u = u + '&repository=%s' % repo
+ for a in arch:
+ u = u + '&arch=%s' % a
+ f = http_GET(u)
+ return f.readlines()
+
+
+def show_prj_results_meta(apiurl, prj):
+ u = makeurl(apiurl, ['build', prj, '_result'])
+ f = http_GET(u)
+ return f.readlines()
+
+
+def result_xml_to_dicts(xml):
+ # assumption: xml contains at most one status element (maybe we should
+ # generalize this to arbitrary status element)
+ root = ET.fromstring(xml)
+ r = []
+ for node in root.findall('result'):
+ rmap = {}
+ rmap['project'] = rmap['prj'] = node.get('project')
+ rmap['repository'] = rmap['repo'] = rmap['rep'] = node.get('repository')
+ rmap['arch'] = node.get('arch')
+ rmap['state'] = node.get('state')
+ rmap['dirty'] = node.get('dirty')
+ rmap['repostate'] = node.get('code')
+ rmap['pkg'] = rmap['package'] = rmap['pac'] = ''
+ rmap['code'] = ''
+ rmap['details'] = ''
+ details = None
+ statusnode = node.find('status')
+ if statusnode is not None:
+ # the way currently use this function, there should be
+ # always a status element
+ rmap['pkg'] = rmap['package'] = rmap['pac'] = statusnode.get('package')
+ rmap['code'] = statusnode.get('code', '')
+ details = statusnode.find('details')
+ if details is not None:
+ rmap['details'] = details.text
+ rmap['dirty'] = rmap['dirty'] == 'true'
+
+ r.append(rmap)
+ return r
+
+
+def format_results(results, format):
+ """apply selected format on each dict in results and return it as a list of strings"""
+ return [format % r for r in results]
+
+
+def get_results(apiurl, project, package, verbose=False, printJoin='', *args, **kwargs):
+ """returns list of/or prints a human readable status for the specified package"""
+ # hmm the function name is a bit too generic - something like
+ # get_package_results_human would be better, but this would break the existing
+ # api (unless we keep get_results around as well)...
+ result_line_templ = '%(rep)-20s %(arch)-10s %(status)s'
+ r = []
+ for results in get_package_results(apiurl, project, package, **kwargs):
+ r = []
+ for res in result_xml_to_dicts(results):
+ if '_oldstate' in res:
+ oldstate = res['_oldstate']
+ continue
+ res['status'] = res['code']
+ if verbose and res['details'] != '':
+ if res['code'] in ('unresolvable', 'expansion error'):
+ lines = res['details'].split(',')
+ res['status'] += ': ' + '\n '.join(lines)
+ else:
+ res['status'] += ': %s' % (res['details'], )
+ if res['dirty']:
+ if verbose:
+ res['status'] = 'outdated (was: %s)' % res['status']
+ else:
+ res['status'] += '*'
+ elif res['code'] in ('succeeded') and res['repostate'] != "published":
+ if verbose:
+ res['status'] += '(unpublished)'
+ else:
+ res['status'] += '*'
+
+ r.append(result_line_templ % res)
+
+ if printJoin:
+ print(printJoin.join(r))
+ return r
+
+
+def get_package_results(apiurl, project, package, wait=False, *args, **kwargs):
+ """generator that returns a the package results as an xml structure"""
+ xml = ''
+ waiting_states = ('blocked', 'scheduled', 'dispatching', 'building',
+ 'signing', 'finished')
+ while True:
+ waiting = False
+ try:
+ xml = ''.join(show_results_meta(apiurl, project, package, *args, **kwargs))
+ except HTTPError as e:
+ # check for simple timeout error and fetch again
+ if e.code == 502 or e.code == 504:
+ # re-try result request
+ continue
+ raise
+ root = ET.fromstring(xml)
+ kwargs['oldstate'] = root.get('state')
+ for result in root.findall('result'):
+ if result.get('dirty') is not None:
+ waiting = True
+ break
+ elif result.get('code') in waiting_states:
+ waiting = True
+ break
+ elif (result.get('code') == 'succeeded'
+ and result.get('repostate') != 'published'):
+ waiting = True
+ break
+
+ if not wait or not waiting:
+ break
+ else:
+ yield xml
+ yield xml
+
+
+def get_prj_results(apiurl, prj, hide_legend=False, csv=False, status_filter=None, name_filter=None, arch=None, repo=None, vertical=None, show_excluded=None):
+ #print '----------------------------------------'
+ global buildstatus_symbols
+
+ r = []
+
+ f = show_prj_results_meta(apiurl, prj)
+ root = ET.fromstring(''.join(f))
+
+ pacs = []
+ # sequence of (repo,arch) tuples
+ targets = []
+ # {package: {(repo,arch): status}}
+ status = {}
+ if root.find('result') == None:
+ return []
+ for results in root.findall('result'):
+ for node in results:
+ pacs.append(node.get('package'))
+ pacs = sorted(list(set(pacs)))
+ for node in root.findall('result'):
+ # filter architecture and repository
+ if arch != None and node.get('arch') not in arch:
+ continue
+ if repo != None and node.get('repository') not in repo:
+ continue
+ if node.get('dirty') == "true":
+ state = "outdated"
+ else:
+ state = node.get('state')
+ tg = (node.get('repository'), node.get('arch'), state)
+ targets.append(tg)
+ for pacnode in node.findall('status'):
+ pac = pacnode.get('package')
+ if pac not in status:
+ status[pac] = {}
+ status[pac][tg] = pacnode.get('code')
+ targets.sort()
+
+ # filter option
+ if status_filter or name_filter or not show_excluded:
+
+ pacs_to_show = []
+ targets_to_show = []
+
+ #filtering for Package Status
+ if status_filter:
+ if status_filter in buildstatus_symbols.values():
+ # a list is needed because if status_filter == "U"
+ # we have to filter either an "expansion error" (obsolete)
+ # or an "unresolvable" state
+ filters = []
+ for txt, sym in buildstatus_symbols.items():
+ if sym == status_filter:
+ filters.append(txt)
+ for filt_txt in filters:
+ for pkg in status.keys():
+ for repo in status[pkg].keys():
+ if status[pkg][repo] == filt_txt:
+ if not name_filter:
+ pacs_to_show.append(pkg)
+ targets_to_show.append(repo)
+ elif name_filter in pkg:
+ pacs_to_show.append(pkg)
+
+ #filtering for Package Name
+ elif name_filter:
+ for pkg in pacs:
+ if name_filter in pkg:
+ pacs_to_show.append(pkg)
+
+ #filter non building states
+ elif not show_excluded:
+ enabled = {}
+ for pkg in status.keys():
+ showpkg = False
+ for repo in status[pkg].keys():
+ if status[pkg][repo] != "excluded":
+ enabled[repo] = 1
+ showpkg = True
+
+ if showpkg:
+ pacs_to_show.append(pkg)
+
+ targets_to_show = enabled.keys()
+
+ pacs = [ i for i in pacs if i in pacs_to_show ]
+ if len(targets_to_show):
+ targets = [ i for i in targets if i in targets_to_show ]
+
+ # csv output
+ if csv:
+ # TODO: option to disable the table header
+ row = ['_'] + ['/'.join(tg) for tg in targets]
+ r.append(';'.join(row))
+ for pac in pacs:
+ row = [pac] + [status[pac][tg] for tg in targets if tg in status[pac]]
+ r.append(';'.join(row))
+ return r
+
+ if not vertical:
+ # human readable output
+ max_pacs = 40
+ for startpac in range(0, len(pacs), max_pacs):
+ offset = 0
+ for pac in pacs[startpac:startpac+max_pacs]:
+ r.append(' |' * offset + ' ' + pac)
+ offset += 1
+
+ for tg in targets:
+ line = []
+ line.append(' ')
+ for pac in pacs[startpac:startpac+max_pacs]:
+ st = ''
+ if pac not in status or tg not in status[pac]:
+ # for newly added packages, status may be missing
+ st = '?'
+ else:
+ try:
+ st = buildstatus_symbols[status[pac][tg]]
+ except:
+ print('osc: warn: unknown status \'%s\'...' % status[pac][tg])
+ print('please edit osc/core.py, and extend the buildstatus_symbols dictionary.')
+ st = '?'
+ buildstatus_symbols[status[pac][tg]] = '?'
+ line.append(st)
+ line.append(' ')
+ line.append(' %s %s (%s)' % tg)
+ line = ''.join(line)
+
+ r.append(line)
+
+ r.append('')
+ else:
+ offset = 0
+ for tg in targets:
+ r.append('| ' * offset + '%s %s (%s)'%tg )
+ offset += 1
+
+ for pac in pacs:
+ line = []
+ for tg in targets:
+ st = ''
+ if pac not in status or tg not in status[pac]:
+ # for newly added packages, status may be missing
+ st = '?'
+ else:
+ try:
+ st = buildstatus_symbols[status[pac][tg]]
+ except:
+ print('osc: warn: unknown status \'%s\'...' % status[pac][tg])
+ print('please edit osc/core.py, and extend the buildstatus_symbols dictionary.')
+ st = '?'
+ buildstatus_symbols[status[pac][tg]] = '?'
+ line.append(st)
+ line.append(' '+pac)
+ r.append(' '.join(line))
+
+ line = []
+ for i in range(0, len(targets)):
+ line.append(str(i%10))
+ r.append(' '.join(line))
+
+ r.append('')
+
+ if not hide_legend and len(pacs):
+ r.append(' Legend:')
+ legend = []
+ for i, j in buildstatus_symbols.items():
+ if i == "expansion error":
+ continue
+ legend.append('%3s %-20s' % (j, i))
+ legend.append(' ? buildstatus not available (only new packages)')
+
+ if vertical:
+ for i in range(0, len(targets)):
+ s = '%1d %s %s (%s)' % (i%10, targets[i][0], targets[i][1], targets[i][2])
+ if i < len(legend):
+ legend[i] += s
+ else:
+ legend.append(' '*24 + s)
+
+ r += legend
+
+ return r
+
+
+def streamfile(url, http_meth = http_GET, bufsize=8192, data=None, progress_obj=None, text=None):
+ """
+ performs http_meth on url and read bufsize bytes from the response
+ until EOF is reached. After each read bufsize bytes are yielded to the
+ caller. A spezial usage is bufsize="line" to read line by line (text).
+ """
+ cl = ''
+ retries = 0
+ # Repeat requests until we get reasonable Content-Length header
+ # Server (or iChain) is corrupting data at some point, see bnc#656281
+ while cl == '':
+ if retries >= int(conf.config['http_retries']):
+ raise oscerr.OscIOError(None, 'Content-Length is empty for %s, protocol violation' % url)
+ retries = retries + 1
+ if retries > 1 and conf.config['http_debug']:
+ print('\n\nRetry %d --' % (retries - 1), url, file=sys.stderr)
+ f = http_meth.__call__(url, data = data)
+ cl = f.info().get('Content-Length')
+
+ if cl is not None:
+ # sometimes the proxy adds the same header again
+ # which yields in value like '3495, 3495'
+ # use the first of these values (should be all the same)
+ cl = cl.split(',')[0]
+ cl = int(cl)
+
+ if progress_obj:
+ basename = os.path.basename(urlsplit(url)[2])
+ progress_obj.start(basename=basename, text=text, size=cl)
+
+ if bufsize == "line":
+ bufsize = 8192
+ xread = f.readline
+ else:
+ xread = f.read
+
+ read = 0
+ while True:
+ data = xread(bufsize)
+ if not len(data):
+ break
+ read += len(data)
+ if progress_obj:
+ progress_obj.update(read)
+ yield data
+
+ if progress_obj:
+ progress_obj.end(read)
+ f.close()
+
+ if not cl is None and read != cl:
+ raise oscerr.OscIOError(None, 'Content-Length is not matching file size for %s: %i vs %i file size' % (url, cl, read))
+
+
+def buildlog_strip_time(data):
+ """Strips the leading build time from the log"""
+ time_regex = re.compile('^\[[^\]]*\] ', re.M)
+ return time_regex.sub('', data)
+
+
+def print_buildlog(apiurl, prj, package, repository, arch, offset=0, strip_time=False, last=False):
+ """prints out the buildlog on stdout"""
+
+ # to protect us against control characters
+ import string
+ all_bytes = string.maketrans('', '')
+ remove_bytes = all_bytes[:8] + all_bytes[14:32] # accept tabs and newlines
+
+ query = {'nostream' : '1', 'start' : '%s' % offset}
+ if last:
+ query['last'] = 1
+ while True:
+ query['start'] = offset
+ start_offset = offset
+ u = makeurl(apiurl, ['build', prj, repository, arch, package, '_log'], query=query)
+ for data in streamfile(u, bufsize="line"):
+ offset += len(data)
+ if strip_time:
+ data = buildlog_strip_time(data)
+ sys.stdout.write(data.translate(all_bytes, remove_bytes))
+ if start_offset == offset:
+ break
+
+def get_dependson(apiurl, project, repository, arch, packages=None, reverse=None):
+ query = []
+ if packages:
+ for i in packages:
+ query.append('package=%s' % quote_plus(i))
+
+ if reverse:
+ query.append('view=revpkgnames')
+ else:
+ query.append('view=pkgnames')
+
+ u = makeurl(apiurl, ['build', project, repository, arch, '_builddepinfo'], query=query)
+ f = http_GET(u)
+ return f.read()
+
+def get_buildinfo(apiurl, prj, package, repository, arch, specfile=None, addlist=None, debug=None):
+ query = []
+ if addlist:
+ for i in addlist:
+ query.append('add=%s' % quote_plus(i))
+ if debug:
+ query.append('debug=1')
+
+ u = makeurl(apiurl, ['build', prj, repository, arch, package, '_buildinfo'], query=query)
+
+ if specfile:
+ f = http_POST(u, data=specfile)
+ else:
+ f = http_GET(u)
+ return f.read()
+
+
+def get_buildconfig(apiurl, prj, repository):
+ u = makeurl(apiurl, ['build', prj, repository, '_buildconfig'])
+ f = http_GET(u)
+ return f.read()
+
+
+def get_source_rev(apiurl, project, package, revision=None):
+ # API supports ?deleted=1&meta=1&rev=4
+ # but not rev=current,rev=latest,rev=top, or anything like this.
+ # CAUTION: We have to loop through all rev and find the highest one, if none given.
+
+ if revision:
+ url = makeurl(apiurl, ['source', project, package, '_history'], {'rev': revision})
+ else:
+ url = makeurl(apiurl, ['source', project, package, '_history'])
+ f = http_GET(url)
+ xml = ET.parse(f)
+ ent = None
+ for new in xml.findall('revision'):
+ # remember the newest one.
+ if not ent:
+ ent = new
+ elif ent.find('time').text < new.find('time').text:
+ ent = new
+ if not ent:
+ return { 'version': None, 'error': 'empty revisionlist: no such package?' }
+ e = {}
+ for k in ent.keys():
+ e[k] = ent.get(k)
+ for k in list(ent):
+ e[k.tag] = k.text
+ return e
+
+def get_buildhistory(apiurl, prj, package, repository, arch, format = 'text', limit = None):
+ import time
+ query = {}
+ if limit != None and int(limit) > 0:
+ query['limit'] = int(limit)
+ u = makeurl(apiurl, ['build', prj, repository, arch, package, '_history'], query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+
+ r = []
+ for node in root.findall('entry'):
+ rev = node.get('rev')
+ srcmd5 = node.get('srcmd5')
+ versrel = node.get('versrel')
+ bcnt = int(node.get('bcnt'))
+ t = time.gmtime(int(node.get('time')))
+ t = time.strftime('%Y-%m-%d %H:%M:%S', t)
+
+ if format == 'csv':
+ r.append('%s|%s|%s|%s.%d' % (t, srcmd5, rev, versrel, bcnt))
+ else:
+ bversrel='%s.%d' % (versrel, bcnt)
+ r.append('%s %s %s %s' % (t, srcmd5, bversrel.ljust(16)[:16], rev))
+
+ if format == 'text':
+ r.insert(0, 'time srcmd5 vers-rel.bcnt rev')
+
+ return r
+
+def print_jobhistory(apiurl, prj, current_package, repository, arch, format = 'text', limit=20):
+ import time
+ query = {}
+ if current_package:
+ query['package'] = current_package
+ if limit != None and int(limit) > 0:
+ query['limit'] = int(limit)
+ u = makeurl(apiurl, ['build', prj, repository, arch, '_jobhistory'], query )
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+
+ if format == 'text':
+ print("time package reason code build time worker")
+ for node in root.findall('jobhist'):
+ package = node.get('package')
+ worker = node.get('workerid')
+ reason = node.get('reason')
+ if not reason:
+ reason = "unknown"
+ code = node.get('code')
+ st = int(node.get('starttime'))
+ et = int(node.get('endtime'))
+ endtime = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(et))
+ waittm = et-st
+ if waittm > 24*60*60:
+ waitbuild = "%1dd %2dh %2dm %2ds" % (waittm / (24*60*60), (waittm / (60*60)) % 24, (waittm / 60) % 60, waittm % 60)
+ elif waittm > 60*60:
+ waitbuild = " %2dh %2dm %2ds" % (waittm / (60*60), (waittm / 60) % 60, waittm % 60)
+ else:
+ waitbuild = " %2dm %2ds" % (waittm / 60, waittm % 60)
+
+ if format == 'csv':
+ print('%s|%s|%s|%s|%s|%s' % (endtime, package, reason, code, waitbuild, worker))
+ else:
+ print('%s %-50s %-16s %-16s %-16s %-16s' % (endtime, package[0:49], reason[0:15], code[0:15], waitbuild, worker))
+
+
+def get_commitlog(apiurl, prj, package, revision, format = 'text', meta = False, deleted = False, revision_upper=None):
+ import time
+
+ query = {}
+ if deleted:
+ query['deleted'] = 1
+ if meta:
+ query['meta'] = 1
+
+ u = makeurl(apiurl, ['source', prj, package, '_history'], query)
+ f = http_GET(u)
+ root = ET.parse(f).getroot()
+
+ r = []
+ if format == 'xml':
+ r.append('<?xml version="1.0"?>')
+ r.append('<log>')
+ revisions = root.findall('revision')
+ revisions.reverse()
+ for node in revisions:
+ srcmd5 = node.find('srcmd5').text
+ try:
+ rev = int(node.get('rev'))
+ #vrev = int(node.get('vrev')) # what is the meaning of vrev?
+ try:
+ if revision is not None and revision_upper is not None:
+ if rev > int(revision_upper) or rev < int(revision):
+ continue
+ elif revision is not None and rev != int(revision):
+ continue
+ except ValueError:
+ if revision != srcmd5:
+ continue
+ except ValueError:
+ # this part should _never_ be reached but...
+ return [ 'an unexpected error occured - please file a bug' ]
+ version = node.find('version').text
+ user = node.find('user').text
+ try:
+ comment = node.find('comment').text.encode(locale.getpreferredencoding(), 'replace')
+ except:
+ comment = '<no message>'
+ try:
+ requestid = node.find('requestid').text.encode(locale.getpreferredencoding(), 'replace')
+ except:
+ requestid = ""
+ t = time.gmtime(int(node.find('time').text))
+ t = time.strftime('%Y-%m-%d %H:%M:%S', t)
+
+ if format == 'csv':
+ s = '%s|%s|%s|%s|%s|%s|%s' % (rev, user, t, srcmd5, version,
+ comment.replace('\\', '\\\\').replace('\n', '\\n').replace('|', '\\|'), requestid)
+ r.append(s)
+ elif format == 'xml':
+ r.append('<logentry')
+ r.append(' revision="%s" srcmd5="%s">' % (rev, srcmd5))
+ r.append('<author>%s</author>' % user)
+ r.append('<date>%s</date>' % t)
+ r.append('<requestid>%s</requestid>' % requestid)
+ r.append('<msg>%s</msg>' %
+ comment.replace('&', '&amp;').replace('<', '&gt;').replace('>', '&lt;'))
+ r.append('</logentry>')
+ else:
+ if requestid:
+ requestid = "rq" + requestid
+ s = '-' * 76 + \
+ '\nr%s | %s | %s | %s | %s | %s\n' % (rev, user, t, srcmd5, version, requestid) + \
+ '\n' + comment
+ r.append(s)
+
+ if format not in ['csv', 'xml']:
+ r.append('-' * 76)
+ if format == 'xml':
+ r.append('</log>')
+ return r
+
+
+def runservice(apiurl, prj, package):
+ u = makeurl(apiurl, ['source', prj, package], query={'cmd': 'runservice'})
+
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ e.osc_msg = 'could not trigger service run for project \'%s\' package \'%s\'' % (prj, package)
+ raise
+
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+def waitservice(apiurl, prj, package):
+ u = makeurl(apiurl, ['source', prj, package], query={'cmd': 'waitservice'})
+
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ e.osc_msg = 'The service for project \'%s\' package \'%s\' failed' % (prj, package)
+ raise
+
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+def mergeservice(apiurl, prj, package):
+ # first waiting that the service finishes and that it did not fail
+ waitservice(apiurl, prj, package)
+
+ # real merge
+ u = makeurl(apiurl, ['source', prj, package], query={'cmd': 'mergeservice'})
+
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ e.osc_msg = 'could not merge service files in project \'%s\' package \'%s\'' % (prj, package)
+ raise
+
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+
+def rebuild(apiurl, prj, package, repo, arch, code=None):
+ query = { 'cmd': 'rebuild' }
+ if package:
+ query['package'] = package
+ if repo:
+ query['repository'] = repo
+ if arch:
+ query['arch'] = arch
+ if code:
+ query['code'] = code
+
+ u = makeurl(apiurl, ['build', prj], query=query)
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ e.osc_msg = 'could not trigger rebuild for project \'%s\' package \'%s\'' % (prj, package)
+ raise
+
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+
+def store_read_project(dir):
+ global store
+
+ try:
+ p = open(os.path.join(dir, store, '_project')).readlines()[0].strip()
+ except IOError:
+ msg = 'Error: \'%s\' is not an osc project dir or working copy' % os.path.abspath(dir)
+ if os.path.exists(os.path.join(dir, '.svn')):
+ msg += '\nTry svn instead of osc.'
+ raise oscerr.NoWorkingCopy(msg)
+ return p
+
+
+def store_read_package(dir):
+ global store
+
+ try:
+ p = open(os.path.join(dir, store, '_package')).readlines()[0].strip()
+ except IOError:
+ msg = 'Error: \'%s\' is not an osc package working copy' % os.path.abspath(dir)
+ if os.path.exists(os.path.join(dir, '.svn')):
+ msg += '\nTry svn instead of osc.'
+ raise oscerr.NoWorkingCopy(msg)
+ return p
+
+def store_read_apiurl(dir, defaulturl=True):
+ global store
+
+ fname = os.path.join(dir, store, '_apiurl')
+ try:
+ url = open(fname).readlines()[0].strip()
+ # this is needed to get a proper apiurl
+ # (former osc versions may stored an apiurl with a trailing slash etc.)
+ apiurl = conf.urljoin(*conf.parse_apisrv_url(None, url))
+ except:
+ if not defaulturl:
+ if is_project_dir(dir):
+ project = store_read_project(dir)
+ package = None
+ elif is_package_dir(dir):
+ project = store_read_project(dir)
+ package = None
+ else:
+ msg = 'Error: \'%s\' is not an osc package working copy' % os.path.abspath(dir)
+ raise oscerr.NoWorkingCopy(msg)
+ msg = 'Your working copy \'%s\' is in an inconsistent state.\n' \
+ 'Please run \'osc repairwc %s\' (Note this might _remove_\n' \
+ 'files from the .osc/ dir). Please check the state\n' \
+ 'of the working copy afterwards (via \'osc status %s\')' % (dir, dir, dir)
+ raise oscerr.WorkingCopyInconsistent(project, package, ['_apiurl'], msg)
+ apiurl = conf.config['apiurl']
+ return apiurl
+
+def store_write_string(dir, file, string, subdir=''):
+ global store
+
+ if subdir and not os.path.isdir(os.path.join(dir, store, subdir)):
+ os.mkdir(os.path.join(dir, store, subdir))
+ fname = os.path.join(dir, store, subdir, file)
+ try:
+ f = open(fname + '.new', 'w')
+ f.write(string)
+ f.close()
+ os.rename(fname + '.new', fname)
+ except:
+ if os.path.exists(fname + '.new'):
+ os.unlink(fname + '.new')
+ raise
+
+def store_write_project(dir, project):
+ store_write_string(dir, '_project', project + '\n')
+
+def store_write_apiurl(dir, apiurl):
+ store_write_string(dir, '_apiurl', apiurl + '\n')
+
+def store_unlink_file(dir, file):
+ global store
+
+ try: os.unlink(os.path.join(dir, store, file))
+ except: pass
+
+def store_read_file(dir, file):
+ global store
+
+ try:
+ content = open(os.path.join(dir, store, file)).read()
+ return content
+ except:
+ return None
+
+def store_write_initial_packages(dir, project, subelements):
+ global store
+
+ fname = os.path.join(dir, store, '_packages')
+ root = ET.Element('project', name=project)
+ for elem in subelements:
+ root.append(elem)
+ ET.ElementTree(root).write(fname)
+
+def get_osc_version():
+ return __version__
+
+
+def abortbuild(apiurl, project, package=None, arch=None, repo=None):
+ return cmdbuild(apiurl, 'abortbuild', project, package, arch, repo)
+
+def restartbuild(apiurl, project, package=None, arch=None, repo=None):
+ return cmdbuild(apiurl, 'restartbuild', project, package, arch, repo)
+
+def wipebinaries(apiurl, project, package=None, arch=None, repo=None, code=None):
+ return cmdbuild(apiurl, 'wipe', project, package, arch, repo, code)
+
+
+def cmdbuild(apiurl, cmd, project, package=None, arch=None, repo=None, code=None):
+ query = { 'cmd': cmd }
+ if package:
+ query['package'] = package
+ if arch:
+ query['arch'] = arch
+ if repo:
+ query['repository'] = repo
+ if code:
+ query['code'] = code
+
+ u = makeurl(apiurl, ['build', project], query)
+ try:
+ f = http_POST(u)
+ except HTTPError as e:
+ e.osc_msg = '%s command failed for project %s' % (cmd, project)
+ if package:
+ e.osc_msg += ' package %s' % package
+ if arch:
+ e.osc_msg += ' arch %s' % arch
+ if repo:
+ e.osc_msg += ' repository %s' % repo
+ if code:
+ e.osc_msg += ' code=%s' % code
+ raise
+
+ root = ET.parse(f).getroot()
+ return root.get('code')
+
+
+def parseRevisionOption(string):
+ """
+ returns a tuple which contains the revisions
+ """
+
+ if string:
+ if ':' in string:
+ splitted_rev = string.split(':')
+ try:
+ for i in splitted_rev:
+ int(i)
+ return splitted_rev
+ except ValueError:
+ print('your revision \'%s\' will be ignored' % string, file=sys.stderr)
+ return None, None
+ else:
+ if string.isdigit():
+ return string, None
+ elif string.isalnum() and len(string) == 32:
+ # could be an md5sum
+ return string, None
+ else:
+ print('your revision \'%s\' will be ignored' % string, file=sys.stderr)
+ return None, None
+ else:
+ return None, None
+
+def checkRevision(prj, pac, revision, apiurl=None, meta=False):
+ """
+ check if revision is valid revision, i.e. it is not
+ larger than the upstream revision id
+ """
+ if len(revision) == 32:
+ # there isn't a way to check this kind of revision for validity
+ return True
+ if not apiurl:
+ apiurl = conf.config['apiurl']
+ try:
+ if int(revision) > int(show_upstream_rev(apiurl, prj, pac, meta)) \
+ or int(revision) <= 0:
+ return False
+ else:
+ return True
+ except (ValueError, TypeError):
+ return False
+
+def build_table(col_num, data = [], headline = [], width=1, csv = False):
+ """
+ This method builds a simple table.
+ Example1: build_table(2, ['foo', 'bar', 'suse', 'osc'], ['col1', 'col2'], 2)
+ col1 col2
+ foo bar
+ suse osc
+ """
+
+ longest_col = []
+ for i in range(col_num):
+ longest_col.append(0)
+ if headline and not csv:
+ data[0:0] = headline
+ # find longest entry in each column
+ i = 0
+ for itm in data:
+ if longest_col[i] < len(itm):
+ longest_col[i] = len(itm)
+ if i == col_num - 1:
+ i = 0
+ else:
+ i += 1
+ # calculate length for each column
+ for i, row in enumerate(longest_col):
+ longest_col[i] = row + width
+ # build rows
+ row = []
+ table = []
+ i = 0
+ for itm in data:
+ if i % col_num == 0:
+ i = 0
+ row = []
+ table.append(row)
+ # there is no need to justify the entries of the last column
+ # or when generating csv
+ if i == col_num -1 or csv:
+ row.append(itm)
+ else:
+ row.append(itm.ljust(longest_col[i]))
+ i += 1
+ if csv:
+ separator = '|'
+ else:
+ separator = ''
+ return [separator.join(row) for row in table]
+
+def xpath_join(expr, new_expr, op='or', inner=False, nexpr_parentheses=False):
+ """
+ Join two xpath expressions. If inner is False expr will
+ be surrounded with parentheses (unless it's not already
+ surrounded). If nexpr_parentheses is True new_expr will be
+ surrounded with parentheses.
+ """
+ if not expr:
+ return new_expr
+ elif not new_expr:
+ return expr
+ # NOTE: this is NO syntax check etc. (e.g. if a literal contains a '(' or ')'
+ # the check might fail and expr will be surrounded with parentheses or NOT)
+ parentheses = not inner
+ if not inner and expr.startswith('(') and expr.endswith(')'):
+ parentheses = False
+ braces = [i for i in expr if i == '(' or i == ')']
+ closed = 0
+ while len(braces):
+ if braces.pop() == ')':
+ closed += 1
+ continue
+ else:
+ closed += -1
+ while len(braces):
+ if braces.pop() == '(':
+ closed += -1
+ else:
+ closed += 1
+ if closed != 0:
+ parentheses = True
+ break
+ if parentheses:
+ expr = '(%s)' % expr
+ if nexpr_parentheses:
+ new_expr = '(%s)' % new_expr
+ return '%s %s %s' % (expr, op, new_expr)
+
+def search(apiurl, **kwargs):
+ """
+ Perform a search request. The requests are constructed as follows:
+ kwargs = {'kind1' => xpath1, 'kind2' => xpath2, ..., 'kindN' => xpathN}
+ GET /search/kind1?match=xpath1
+ ...
+ GET /search/kindN?match=xpathN
+ """
+ res = {}
+ for urlpath, xpath in kwargs.items():
+ path = [ 'search' ]
+ path += urlpath.split('_') # FIXME: take underscores as path seperators. I see no other way atm to fix OBS api calls and not breaking osc api
+ u = makeurl(apiurl, path, ['match=%s' % quote_plus(xpath)])
+ f = http_GET(u)
+ res[urlpath] = ET.parse(f).getroot()
+ return res
+
+def owner(apiurl, binary, mode="binary", attribute=None, project=None, usefilter=None, devel=None, limit=None):
+ """
+ Perform a binary package owner search. This is supported since OBS 2.4.
+ """
+ # find default project, if not specified
+ query = { mode: binary }
+ if attribute:
+ query['attribute'] = attribute
+ if project:
+ query['project'] = project
+ if devel:
+ query['devel'] = devel
+ if limit != None:
+ query['limit'] = limit
+ if usefilter != None:
+ query['filter'] = ",".join(usefilter)
+ u = makeurl(apiurl, [ 'search', 'owner' ], query)
+ res = None
+ try:
+ f = http_GET(u)
+ res = ET.parse(f).getroot()
+ except HTTPError as e:
+ # old server not supporting this search
+ pass
+ return res
+
+def set_link_rev(apiurl, project, package, revision='', expand=False):
+ url = makeurl(apiurl, ['source', project, package, '_link'])
+ try:
+ f = http_GET(url)
+ root = ET.parse(f).getroot()
+ except HTTPError as e:
+ e.osc_msg = 'Unable to get _link file in package \'%s\' for project \'%s\'' % (package, project)
+ raise
+ revision = _set_link_rev(apiurl, project, package, root, revision, expand=expand)
+ l = ET.tostring(root, encoding=ET_ENCODING)
+ http_PUT(url, data=l)
+ return revision
+
+def _set_link_rev(apiurl, project, package, root, revision='', expand=False):
+ """
+ Updates the rev attribute of the _link xml. If revision is set to None
+ the rev and vrev attributes are removed from the _link xml.
+ updates the rev attribute of the _link xml. If revision is the empty
+ string the latest rev of the link's source package is used (or the
+ xsrcmd5 if expand is True). If revision is neither None nor the empty
+ string the _link's rev attribute is set to this revision (or to the
+ xsrcmd5 if expand is True).
+ """
+ src_project = root.get('project', project)
+ src_package = root.get('package', package)
+ vrev = None
+ if revision is None:
+ if 'rev' in root.keys():
+ del root.attrib['rev']
+ if 'vrev' in root.keys():
+ del root.attrib['vrev']
+ elif not revision or expand:
+ revision, vrev = show_upstream_rev_vrev(apiurl, src_project, src_package, revision=revision, expand=expand)
+
+ if revision:
+ root.set('rev', revision)
+ # add vrev when revision is a srcmd5
+ if vrev is not None and revision is not None and len(revision) >= 32:
+ root.set('vrev', vrev)
+ return revision
+
+
+def delete_dir(dir):
+ # small security checks
+ if os.path.islink(dir):
+ raise oscerr.OscIOError(None, 'cannot remove linked dir')
+ elif os.path.abspath(dir) == '/':
+ raise oscerr.OscIOError(None, 'cannot remove \'/\'')
+
+ for dirpath, dirnames, filenames in os.walk(dir, topdown=False):
+ for filename in filenames:
+ os.unlink(os.path.join(dirpath, filename))
+ for dirname in dirnames:
+ os.rmdir(os.path.join(dirpath, dirname))
+ os.rmdir(dir)
+
+
+def delete_storedir(store_dir):
+ """
+ This method deletes a store dir.
+ """
+ head, tail = os.path.split(store_dir)
+ if tail == '.osc':
+ delete_dir(store_dir)
+
+def unpack_srcrpm(srpm, dir, *files):
+ """
+ This method unpacks the passed srpm into the
+ passed dir. If arguments are passed to the \'files\' tuple
+ only this files will be unpacked.
+ """
+ if not is_srcrpm(srpm):
+ print('error - \'%s\' is not a source rpm.' % srpm, file=sys.stderr)
+ sys.exit(1)
+ curdir = os.getcwd()
+ if os.path.isdir(dir):
+ os.chdir(dir)
+ cmd = 'rpm2cpio %s | cpio -i %s &> /dev/null' % (srpm, ' '.join(files))
+ ret = run_external(cmd, shell=True)
+ if ret != 0:
+ print('error \'%s\' - cannot extract \'%s\'' % (ret, srpm), file=sys.stderr)
+ sys.exit(1)
+ os.chdir(curdir)
+
+def is_rpm(f):
+ """check if the named file is an RPM package"""
+ try:
+ h = open(f, 'rb').read(4)
+ except:
+ return False
+
+ if h == '\xed\xab\xee\xdb':
+ return True
+ else:
+ return False
+
+def is_srcrpm(f):
+ """check if the named file is a source RPM"""
+
+ if not is_rpm(f):
+ return False
+
+ try:
+ h = open(f, 'rb').read(8)
+ except:
+ return False
+
+ if h[7] == '\x01':
+ return True
+ else:
+ return False
+
+def addMaintainer(apiurl, prj, pac, user):
+ # for backward compatibility only
+ addPerson(apiurl, prj, pac, user)
+
+def addPerson(apiurl, prj, pac, user, role="maintainer"):
+ """ add a new person to a package or project """
+ path = quote_plus(prj),
+ kind = 'prj'
+ if pac:
+ path = path + (quote_plus(pac),)
+ kind = 'pkg'
+ data = meta_exists(metatype=kind,
+ path_args=path,
+ template_args=None,
+ create_new=False)
+
+ if data and get_user_meta(apiurl, user) != None:
+ root = ET.fromstring(''.join(data))
+ found = False
+ for person in root.getiterator('person'):
+ if person.get('userid') == user and person.get('role') == role:
+ found = True
+ print("user already exists")
+ break
+ if not found:
+ # the xml has a fixed structure
+ root.insert(2, ET.Element('person', role=role, userid=user))
+ print('user \'%s\' added to \'%s\'' % (user, pac or prj))
+ edit_meta(metatype=kind,
+ path_args=path,
+ data=ET.tostring(root, encoding=ET_ENCODING))
+ else:
+ print("osc: an error occured")
+
+def delMaintainer(apiurl, prj, pac, user):
+ # for backward compatibility only
+ delPerson(apiurl, prj, pac, user)
+
+def delPerson(apiurl, prj, pac, user, role="maintainer"):
+ """ delete a person from a package or project """
+ path = quote_plus(prj),
+ kind = 'prj'
+ if pac:
+ path = path + (quote_plus(pac), )
+ kind = 'pkg'
+ data = meta_exists(metatype=kind,
+ path_args=path,
+ template_args=None,
+ create_new=False)
+ if data and get_user_meta(apiurl, user) != None:
+ root = ET.fromstring(''.join(data))
+ found = False
+ for person in root.getiterator('person'):
+ if person.get('userid') == user and person.get('role') == role:
+ root.remove(person)
+ found = True
+ print("user \'%s\' removed" % user)
+ if found:
+ edit_meta(metatype=kind,
+ path_args=path,
+ data=ET.tostring(root, encoding=ET_ENCODING))
+ else:
+ print("user \'%s\' not found in \'%s\'" % (user, pac or prj))
+ else:
+ print("an error occured")
+
+def setBugowner(apiurl, prj, pac, user=None, group=None):
+ """ delete all bugowners (user and group entries) and set one new one in a package or project """
+ path = quote_plus(prj),
+ kind = 'prj'
+ if pac:
+ path = path + (quote_plus(pac), )
+ kind = 'pkg'
+ data = meta_exists(metatype=kind,
+ path_args=path,
+ template_args=None,
+ create_new=False)
+ if user.startswith('group:'):
+ group=user.replace('group:', '')
+ user=None
+ if data:
+ root = ET.fromstring(''.join(data))
+ for group_element in root.getiterator('group'):
+ if group_element.get('role') == "bugowner":
+ root.remove(group_element)
+ for person_element in root.getiterator('person'):
+ if person_element.get('role') == "bugowner":
+ root.remove(person_element)
+ if user:
+ root.insert(2, ET.Element('person', role='bugowner', userid=user))
+ elif group:
+ root.insert(2, ET.Element('group', role='bugowner', groupid=group))
+ else:
+ print("Neither user nor group is specified")
+ edit_meta(metatype=kind,
+ path_args=path,
+ data=ET.tostring(root, encoding=ET_ENCODING))
+
+def setDevelProject(apiurl, prj, pac, dprj, dpkg=None):
+ """ set the <devel project="..."> element to package metadata"""
+ path = (quote_plus(prj),) + (quote_plus(pac),)
+ data = meta_exists(metatype='pkg',
+ path_args=path,
+ template_args=None,
+ create_new=False)
+
+ if data and show_project_meta(apiurl, dprj) != None:
+ root = ET.fromstring(''.join(data))
+ if not root.find('devel') != None:
+ ET.SubElement(root, 'devel')
+ elem = root.find('devel')
+ if dprj:
+ elem.set('project', dprj)
+ else:
+ if 'project' in elem.keys():
+ del elem.attrib['project']
+ if dpkg:
+ elem.set('package', dpkg)
+ else:
+ if 'package' in elem.keys():
+ del elem.attrib['package']
+ edit_meta(metatype='pkg',
+ path_args=path,
+ data=ET.tostring(root, encoding=ET_ENCODING))
+ else:
+ print("osc: an error occured")
+
+def createPackageDir(pathname, prj_obj=None):
+ """
+ create and initialize a new package dir in the given project.
+ prj_obj can be a Project() instance.
+ """
+ prj_dir, pac_dir = getPrjPacPaths(pathname)
+ if is_project_dir(prj_dir):
+ global store
+ if not os.path.exists(pac_dir+store):
+ prj = prj_obj or Project(prj_dir, False)
+ Package.init_package(prj.apiurl, prj.name, pac_dir, pac_dir)
+ prj.addPackage(pac_dir)
+ print(statfrmt('A', os.path.normpath(pathname)))
+ else:
+ raise oscerr.OscIOError(None, 'file or directory \'%s\' already exists' % pathname)
+ else:
+ msg = '\'%s\' is not a working copy' % prj_dir
+ if os.path.exists(os.path.join(prj_dir, '.svn')):
+ msg += '\ntry svn instead of osc.'
+ raise oscerr.NoWorkingCopy(msg)
+
+
+def stripETxml(node):
+ node.tail = None
+ if node.text != None:
+ node.text = node.text.replace(" ", "").replace("\n", "")
+ for child in node.getchildren():
+ stripETxml(child)
+
+def addGitSource(url):
+ service_file = os.path.join(os.getcwd(), '_service')
+ addfile = False
+ if os.path.exists( service_file ):
+ services = ET.parse(os.path.join(os.getcwd(), '_service')).getroot()
+ else:
+ services = ET.fromstring("<services />")
+ addfile = True
+ stripETxml( services )
+ si = Serviceinfo()
+ s = si.addGitUrl(services, url)
+ s = si.addTarUp(services)
+ s = si.addRecompressTar(services)
+ s = si.addSetVersion(services)
+ si.read(s)
+
+ # for pretty output
+ xmlindent(s)
+ f = open(service_file, 'wb')
+ f.write(ET.tostring(s, encoding=ET_ENCODING))
+ f.close()
+ if addfile:
+ addFiles( ['_service'] )
+
+def addDownloadUrlService(url):
+ service_file = os.path.join(os.getcwd(), '_service')
+ addfile = False
+ if os.path.exists( service_file ):
+ services = ET.parse(os.path.join(os.getcwd(), '_service')).getroot()
+ else:
+ services = ET.fromstring("<services />")
+ addfile = True
+ stripETxml( services )
+ si = Serviceinfo()
+ s = si.addDownloadUrl(services, url)
+ si.read(s)
+
+ # for pretty output
+ xmlindent(s)
+ f = open(service_file, 'wb')
+ f.write(ET.tostring(s, encoding=ET_ENCODING))
+ f.close()
+ if addfile:
+ addFiles( ['_service'] )
+
+ # download file
+ path = os.getcwd()
+ files = os.listdir(path)
+ si.execute(path)
+ newfiles = os.listdir(path)
+
+ # add verify service for new files
+ for filename in files:
+ newfiles.remove(filename)
+
+ for filename in newfiles:
+ if filename.startswith('_service:download_url:'):
+ s = si.addVerifyFile(services, filename)
+
+ # for pretty output
+ xmlindent(s)
+ f = open(service_file, 'wb')
+ f.write(ET.tostring(s, encoding=ET_ENCODING))
+ f.close()
+
+
+def addFiles(filenames, prj_obj = None):
+ for filename in filenames:
+ if not os.path.exists(filename):
+ raise oscerr.OscIOError(None, 'file \'%s\' does not exist' % filename)
+
+ # init a package dir if we have a normal dir in the "filenames"-list
+ # so that it will be find by findpacs() later
+ pacs = list(filenames)
+ for filename in filenames:
+ prj_dir, pac_dir = getPrjPacPaths(filename)
+ if not is_package_dir(filename) and os.path.isdir(filename) and is_project_dir(prj_dir) \
+ and conf.config['do_package_tracking']:
+ prj_name = store_read_project(prj_dir)
+ prj_apiurl = store_read_apiurl(prj_dir, defaulturl=False)
+ Package.init_package(prj_apiurl, prj_name, pac_dir, filename)
+ elif is_package_dir(filename) and conf.config['do_package_tracking']:
+ raise oscerr.PackageExists(store_read_project(filename), store_read_package(filename),
+ 'osc: warning: \'%s\' is already under version control' % filename)
+ elif os.path.isdir(filename) and is_project_dir(prj_dir):
+ raise oscerr.WrongArgs('osc: cannot add a directory to a project unless ' \
+ '\'do_package_tracking\' is enabled in the configuration file')
+ elif os.path.isdir(filename):
+ print('skipping directory \'%s\'' % filename)
+ pacs.remove(filename)
+ pacs = findpacs(pacs)
+ for pac in pacs:
+ if conf.config['do_package_tracking'] and not pac.todo:
+ prj = prj_obj or Project(os.path.dirname(pac.absdir), False)
+ if pac.name in prj.pacs_unvers:
+ prj.addPackage(pac.name)
+ print(statfrmt('A', getTransActPath(os.path.join(pac.dir, os.pardir, pac.name))))
+ for filename in pac.filenamelist_unvers:
+ if os.path.isdir(os.path.join(pac.dir, filename)):
+ print('skipping directory \'%s\'' % os.path.join(pac.dir, filename))
+ else:
+ pac.todo.append(filename)
+ elif pac.name in prj.pacs_have:
+ print('osc: warning: \'%s\' is already under version control' % pac.name)
+ for filename in pac.todo:
+ if filename in pac.skipped:
+ continue
+ if filename in pac.excluded:
+ print('osc: warning: \'%s\' is excluded from a working copy' % filename, file=sys.stderr)
+ continue
+ pac.addfile(filename)
+
+def getPrjPacPaths(path):
+ """
+ returns the path for a project and a package
+ from path. This is needed if you try to add
+ or delete packages:
+ Examples:
+ osc add pac1/: prj_dir = CWD;
+ pac_dir = pac1
+ osc add /path/to/pac1:
+ prj_dir = path/to;
+ pac_dir = pac1
+ osc add /path/to/pac1/file
+ => this would be an invalid path
+ the caller has to validate the returned
+ path!
+ """
+ # make sure we hddave a dir: osc add bar vs. osc add bar/; osc add /path/to/prj_dir/new_pack
+ # filename = os.path.join(tail, '')
+ prj_dir, pac_dir = os.path.split(os.path.normpath(path))
+ if prj_dir == '':
+ prj_dir = os.getcwd()
+ return (prj_dir, pac_dir)
+
+def getTransActPath(pac_dir):
+ """
+ returns the path for the commit and update operations/transactions.
+ Normally the "dir" attribute of a Package() object will be passed to
+ this method.
+ """
+ if pac_dir != '.':
+ pathn = os.path.normpath(pac_dir)
+ else:
+ pathn = ''
+ return pathn
+
+def get_commit_message_template(pac):
+ """
+ Read the difference in .changes file(s) and put them as a template to commit message.
+ """
+ diff = []
+ template = []
+
+ if pac.todo:
+ todo = pac.todo
+ else:
+ todo = pac.filenamelist + pac.filenamelist_unvers
+
+ files = [i for i in todo if i.endswith('.changes') and pac.status(i) in ('A', 'M')]
+
+ for filename in files:
+ if pac.status(filename) == 'M':
+ diff += get_source_file_diff(pac.absdir, filename, pac.rev)
+ elif pac.status(filename) == 'A':
+ f = open(os.path.join(pac.absdir, filename), 'r')
+ for line in f:
+ diff += '+' + line
+ f.close()
+
+ if diff:
+ template = parse_diff_for_commit_message(''.join(diff))
+
+ return template
+
+def parse_diff_for_commit_message(diff, template = []):
+ date_re = re.compile(r'\+(Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([A-Z][a-z]{2}) ( ?[0-9]|[0-3][0-9]) .*')
+ diff = diff.split('\n')
+
+ # The first four lines contains a header of diff
+ for line in diff[3:]:
+ # this condition is magical, but it removes all unwanted lines from commit message
+ if not(line) or (line and line[0] != '+') or \
+ date_re.match(line) or \
+ line == '+' or line[0:3] == '+++':
+ continue
+
+ if line == '+-------------------------------------------------------------------':
+ template.append('')
+ else:
+ template.append(line[1:])
+
+ return template
+
+def get_commit_msg(wc_dir, pacs):
+ template = store_read_file(wc_dir, '_commit_msg')
+ # open editor for commit message
+ # but first, produce status and diff to append to the template
+ footer = []
+ lines = []
+ for p in pacs:
+ states = sorted(p.get_status(False, ' ', '?'), lambda x, y: cmp(x[1], y[1]))
+ changed = [statfrmt(st, os.path.normpath(os.path.join(p.dir, filename))) for st, filename in states]
+ if changed:
+ footer += changed
+ footer.append('\nDiff for working copy: %s' % p.dir)
+ footer.extend([''.join(i) for i in p.get_diff(ignoreUnversioned=True)])
+ lines.extend(get_commit_message_template(p))
+ if template is None:
+ if lines and lines[0] == '':
+ del lines[0]
+ template = '\n'.join(lines)
+ msg = ''
+ # if footer is empty, there is nothing to commit, and no edit needed.
+ if footer:
+ msg = edit_message(footer='\n'.join(footer), template=template)
+ if msg:
+ store_write_string(wc_dir, '_commit_msg', msg + '\n')
+ else:
+ store_unlink_file(wc_dir, '_commit_msg')
+ return msg
+
+def print_request_list(apiurl, project, package = None, states = ('new', 'review', ), force = False):
+ """
+ prints list of pending requests for the specified project/package if "check_for_request_on_action"
+ is enabled in the config or if "force" is set to True
+ """
+ if not conf.config['check_for_request_on_action'] and not force:
+ return
+ requests = get_request_list(apiurl, project, package, req_state=states)
+ msg = 'Pending requests for %s: %s (%s)'
+ if package is None and len(requests):
+ print(msg % ('project', project, len(requests)))
+ elif len(requests):
+ print(msg % ('package', '/'.join([project, package]), len(requests)))
+ for r in requests:
+ print(r.list_view(), '\n')
+
+def request_interactive_review(apiurl, request, initial_cmd='', group=None,
+ ignore_reviews=False, source_buildstatus=False):
+ """review the request interactively"""
+ import tempfile, re
+
+ tmpfile = None
+
+ def safe_change_request_state(*args, **kwargs):
+ try:
+ change_request_state(*args, **kwargs)
+ return True
+ except HTTPError as e:
+ print('Server returned an error:', e, file=sys.stderr)
+ print('Try -f to force the state change', file=sys.stderr)
+ return False
+
+ def print_request(request):
+ print(request)
+
+ def print_source_buildstatus(src_actions, newline=False):
+ if newline:
+ print()
+ if not src_actions:
+ print('unable to get source buildstatus: no source actions defined')
+ for action in src_actions:
+ print('%s/%s:' % (action.src_project, action.src_package))
+ print('\n'.join(get_results(apiurl, action.src_project, action.src_package)))
+
+ print_request(request)
+ try:
+ prompt = '(a)ccept/(d)ecline/(r)evoke/c(l)one/(s)kip/(c)ancel > '
+ editable_actions = request.get_actions('submit', 'maintenance_incident')
+ # actions which have sources + buildresults
+ src_actions = editable_actions + request.get_actions('maintenance_release')
+ if editable_actions:
+ prompt = 'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/c(l)one/(e)dit/(s)kip/(c)ancel > '
+ elif src_actions:
+ # no edit for maintenance release requests
+ prompt = 'd(i)ff/(a)ccept/(d)ecline/(r)evoke/(b)uildstatus/c(l)one/(s)kip/(c)ancel > '
+ editprj = ''
+ orequest = None
+ if source_buildstatus:
+ print_source_buildstatus(src_actions, newline=True)
+ while True:
+ if initial_cmd:
+ repl = initial_cmd
+ initial_cmd = ''
+ else:
+ repl = raw_input(prompt).strip()
+ if repl == 'i' and src_actions:
+ if not orequest is None and tmpfile:
+ tmpfile.close()
+ tmpfile = None
+ if tmpfile is None:
+ tmpfile = tempfile.NamedTemporaryFile(suffix='.diff')
+ try:
+ diff = request_diff(apiurl, request.reqid)
+ tmpfile.write(diff)
+ except HTTPError as e:
+ if e.code != 400:
+ raise
+ # backward compatible diff for old apis
+ for action in src_actions:
+ diff = 'old: %s/%s\nnew: %s/%s\n' % (action.src_project, action.src_package,
+ action.tgt_project, action.tgt_package)
+ diff += submit_action_diff(apiurl, action)
+ diff += '\n\n'
+ tmpfile.write(diff)
+ tmpfile.flush()
+ run_editor(tmpfile.name)
+ print_request(request)
+ elif repl == 's':
+ print('skipping: #%s' % request.reqid, file=sys.stderr)
+ break
+ elif repl == 'c':
+ print('Aborting', file=sys.stderr)
+ raise oscerr.UserAbort()
+ elif repl == 'b' and src_actions:
+ print_source_buildstatus(src_actions)
+ elif repl == 'e' and editable_actions:
+ # this is only for editable actions
+ if not editprj:
+ editprj = clone_request(apiurl, request.reqid, 'osc editrequest')
+ orequest = request
+ request = edit_submitrequest(apiurl, editprj, orequest, request)
+ src_actions = editable_actions = request.get_actions('submit', 'maintenance_incident')
+ print_request(request)
+ prompt = 'd(i)ff/(a)ccept/(b)uildstatus/(e)dit/(s)kip/(c)ancel > '
+ else:
+ state_map = {'a': 'accepted', 'd': 'declined', 'r': 'revoked'}
+ mo = re.search('^([adrl])(?:\s+(-f)?\s*-m\s+(.*))?$', repl)
+ if mo is None or orequest and mo.group(1) != 'a':
+ print('invalid choice: \'%s\'' % repl, file=sys.stderr)
+ continue
+ state = state_map.get(mo.group(1))
+ force = mo.group(2) is not None
+ msg = mo.group(3)
+ footer = ''
+ msg_template = ''
+ if not (state is None or request.state is None):
+ footer = 'changing request from state \'%s\' to \'%s\'\n\n' \
+ % (request.state.name, state)
+ msg_template = change_request_state_template(request, state)
+ footer += str(request)
+ if tmpfile is not None:
+ tmpfile.seek(0)
+ # the read bytes probably have a moderate size so the str won't be too large
+ footer += '\n\n' + tmpfile.read()
+ if msg is None:
+ try:
+ msg = edit_message(footer = footer, template=msg_template)
+ except oscerr.UserAbort:
+ # do not abort (show prompt again)
+ continue
+ else:
+ msg = msg.strip('\'').strip('"')
+ if not orequest is None:
+ request.create(apiurl)
+ if not safe_change_request_state(apiurl, request.reqid, 'accepted', msg, force=force):
+ # an error occured
+ continue
+ repl = raw_input('Supersede original request? (y|N) ')
+ if repl in ('y', 'Y'):
+ safe_change_request_state(apiurl, orequest.reqid, 'superseded',
+ 'superseded by %s' % request.reqid, request.reqid, force=force)
+ elif state is None:
+ clone_request(apiurl, request.reqid, msg)
+ else:
+ reviews = [r for r in request.reviews if r.state == 'new']
+ if not reviews or ignore_reviews:
+ if safe_change_request_state(apiurl, request.reqid, state, msg, force=force):
+ break
+ else:
+ # an error occured
+ continue
+ group_reviews = [r for r in reviews if (r.by_group is not None
+ and r.by_group == group)]
+ if len(group_reviews) == 1 and conf.config['review_inherit_group']:
+ review = group_reviews[0]
+ else:
+ print('Please chose one of the following reviews:')
+ for i in range(len(reviews)):
+ fmt = Request.format_review(reviews[i])
+ print('(%i)' % i, 'by %(type)-10s %(by)s' % fmt)
+ num = raw_input('> ')
+ try:
+ num = int(num)
+ except ValueError:
+ print('\'%s\' is not a number.' % num)
+ continue
+ if num < 0 or num >= len(reviews):
+ print('number \'%s\' out of range.' % num)
+ continue
+ review = reviews[num]
+ change_review_state(apiurl, request.reqid, state, by_user=review.by_user,
+ by_group=review.by_group, by_project=review.by_project,
+ by_package=review.by_package, message=msg)
+ break
+ finally:
+ if tmpfile is not None:
+ tmpfile.close()
+
+def edit_submitrequest(apiurl, project, orequest, new_request=None):
+ """edit a submit action from orequest/new_request"""
+ import tempfile, shutil
+ actions = orequest.get_actions('submit')
+ oactions = actions
+ if new_request is not None:
+ actions = new_request.get_actions('submit')
+ num = 0
+ if len(actions) > 1:
+ print('Please chose one of the following submit actions:')
+ for i in range(len(actions)):
+ # it is safe to use orequest because currently the formatting
+ # of a submit action does not need instance specific data
+ fmt = orequest.format_action(actions[i])
+ print('(%i)' % i, '%(source)s %(target)s' % fmt)
+ num = raw_input('> ')
+ try:
+ num = int(num)
+ except ValueError:
+ raise oscerr.WrongArgs('\'%s\' is not a number.' % num)
+ if num < 0 or num >= len(orequest.actions):
+ raise oscerr.WrongArgs('number \'%s\' out of range.' % num)
+
+ # the api replaced ':' with '_' in prj and pkg names (clone request)
+ package = '%s.%s' % (oactions[num].src_package.replace(':', '_'),
+ oactions[num].src_project.replace(':', '_'))
+ tmpdir = None
+ cleanup = True
+ try:
+ tmpdir = tempfile.mkdtemp(prefix='osc_editsr')
+ p = Package.init_package(apiurl, project, package, tmpdir)
+ p.update()
+ shell = os.getenv('SHELL', default='/bin/sh')
+ olddir = os.getcwd()
+ os.chdir(tmpdir)
+ print('Checked out package \'%s\' to %s. Started a new shell (%s).\n' \
+ 'Please fix the package and close the shell afterwards.' % (package, tmpdir, shell))
+ run_external(shell)
+ # the pkg might have uncommitted changes...
+ cleanup = False
+ os.chdir(olddir)
+ # reread data
+ p = Package(tmpdir)
+ modified = p.get_status(False, ' ', '?', 'S')
+ if modified:
+ print('Your working copy has the following modifications:')
+ print('\n'.join([statfrmt(st, filename) for st, filename in modified]))
+ repl = raw_input('Do you want to commit the local changes first? (y|N) ')
+ if repl in ('y', 'Y'):
+ msg = get_commit_msg(p.absdir, [p])
+ p.commit(msg=msg)
+ cleanup = True
+ finally:
+ if cleanup:
+ shutil.rmtree(tmpdir)
+ else:
+ print('Please remove the dir \'%s\' manually' % tmpdir)
+ r = Request()
+ for action in orequest.get_actions():
+ new_action = Action.from_xml(action.to_xml())
+ r.actions.append(new_action)
+ if new_action.type == 'submit':
+ new_action.src_package = '%s.%s' % (action.src_package.replace(':', '_'),
+ action.src_project.replace(':', '_'))
+ new_action.src_project = project
+ # do an implicit cleanup
+ new_action.opt_sourceupdate = 'cleanup'
+ return r
+
+def get_user_projpkgs(apiurl, user, role=None, exclude_projects=[], proj=True, pkg=True, maintained=False, metadata=False):
+ """Return all project/packages where user is involved."""
+ xpath = 'person/@userid = \'%s\'' % user
+ excl_prj = ''
+ excl_pkg = ''
+ for i in exclude_projects:
+ excl_prj = xpath_join(excl_prj, 'not(@name = \'%s\')' % i, op='and')
+ excl_pkg = xpath_join(excl_pkg, 'not(@project = \'%s\')' % i, op='and')
+ role_filter_xpath = xpath
+ if role:
+ xpath = xpath_join(xpath, 'person/@role = \'%s\'' % role, inner=True, op='and')
+ xpath_pkg = xpath_join(xpath, excl_pkg, op='and')
+ xpath_prj = xpath_join(xpath, excl_prj, op='and')
+
+ if maintained:
+ xpath_pkg = xpath_join(xpath_pkg, '(project/attribute/@name=\'%(attr)s\' or attribute/@name=\'%(attr)s\')' % {'attr': conf.config['maintained_attribute']}, op='and')
+
+ what = {}
+ if pkg:
+ if metadata:
+ what['package'] = xpath_pkg
+ else:
+ what['package_id'] = xpath_pkg
+ if proj:
+ if metadata:
+ what['project'] = xpath_prj
+ else:
+ what['project_id'] = xpath_prj
+ try:
+ res = search(apiurl, **what)
+ except HTTPError as e:
+ if e.code != 400 or not role_filter_xpath:
+ raise e
+ # backward compatibility: local role filtering
+ what = dict([[kind, role_filter_xpath] for kind in what.keys()])
+ if 'package' in what:
+ what['package'] = xpath_join(role_filter_xpath, excl_pkg, op='and')
+ if 'project' in what:
+ what['project'] = xpath_join(role_filter_xpath, excl_prj, op='and')
+ res = search(apiurl, **what)
+ filter_role(res, user, role)
+ return res
+
+def raw_input(*args):
+ try:
+ import builtins
+ func = builtins.input
+ except ImportError:
+ #python 2.7
+ import __builtin__
+ func = __builtin__.raw_input
+
+ try:
+ return func(*args)
+ except EOFError:
+ # interpret ctrl-d as user abort
+ raise oscerr.UserAbort()
+
+def run_external(filename, *args, **kwargs):
+ """Executes the program filename via subprocess.call.
+
+ *args are additional arguments which are passed to the
+ program filename. **kwargs specify additional arguments for
+ the subprocess.call function.
+ if no args are specified the plain filename is passed
+ to subprocess.call (this can be used to execute a shell
+ command). Otherwise [filename] + list(args) is passed
+ to the subprocess.call function.
+
+ """
+ # unless explicitly specified use shell=False
+ kwargs.setdefault('shell', False)
+ if args:
+ cmd = [filename] + list(args)
+ else:
+ cmd = filename
+ try:
+ return subprocess.call(cmd, **kwargs)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ raise oscerr.ExtRuntimeError(e.strerror, filename)
+
+# backward compatibility: local role filtering
+def filter_role(meta, user, role):
+ """
+ remove all project/package nodes if no person node exists
+ where @userid=user and @role=role
+ """
+ for kind, root in meta.items():
+ delete = []
+ for node in root.findall(kind):
+ found = False
+ for p in node.findall('person'):
+ if p.get('userid') == user and p.get('role') == role:
+ found = True
+ break
+ if not found:
+ delete.append(node)
+ for node in delete:
+ root.remove(node)
+
+def find_default_project(apiurl=None, package=None):
+ """"
+ look though the list of conf.config['getpac_default_project']
+ and find the first project where the given package exists in the build service.
+ """
+ if not len(conf.config['getpac_default_project']):
+ return None
+ candidates = re.split('[, ]+', conf.config['getpac_default_project'])
+ if package is None or len(candidates) == 1:
+ return candidates[0]
+
+ # search through the list, where package exists ...
+ for prj in candidates:
+ try:
+ # any fast query will do here.
+ show_package_meta(apiurl, prj, package)
+ return prj
+ except HTTPError:
+ pass
+ return None
+
+def utime(filename, arg, ignore_einval=True):
+ """wrapper around os.utime which ignore errno EINVAL by default"""
+ try:
+ # workaround for bnc#857610): if filename resides on a nfs share
+ # os.utime might raise EINVAL
+ os.utime(filename, arg)
+ except OSError as e:
+ if e.errno == errno.EINVAL and ignore_einval:
+ return
+ raise
+
+def which(name):
+ """Searches "name" in PATH."""
+ name = os.path.expanduser(name)
+ if os.path.isabs(name):
+ if os.path.exists(name):
+ return name
+ return None
+ for directory in os.environ.get('PATH', '').split(':'):
+ path = os.path.join(directory, name)
+ if os.path.exists(path):
+ return path
+ return None
+
+
+def get_comments(apiurl, kind, name):
+ url = makeurl(apiurl, ['comments', kind, name])
+ f = http_GET(url)
+ return ET.parse(f).getroot()
+
+
+def print_comments(apiurl, kind, name):
+ def print_rec(comments, indent=''):
+ for comment in comments:
+ print(indent, end='')
+ print('On', comment.get('when'), comment.get('who'), 'wrote:')
+ text = indent + comment.text.replace('\r\n',' \n')
+ print(('\n' + indent).join(text.split('\n')))
+ print()
+ print_rec([c for c in root if c.get('parent') == comment.get('id')], indent + ' ')
+
+ root = get_comments(apiurl, kind, name)
+ comments = [c for c in root if c.get('parent') is None]
+ if comments:
+ print('\nComments:')
+ print_rec(comments)
+
+# vim: sw=4 et
diff --git a/osc/fetch.py b/osc/fetch.py
new file mode 100644
index 0000000..21cc3d6
--- /dev/null
+++ b/osc/fetch.py
@@ -0,0 +1,435 @@
+# Copyright (C) 2006 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+from __future__ import print_function
+
+import sys, os
+
+try:
+ from urllib.parse import quote_plus
+ from urllib.request import HTTPBasicAuthHandler, HTTPCookieProcessor, HTTPPasswordMgrWithDefaultRealm, HTTPError
+except ImportError:
+ #python 2.x
+ from urllib import quote_plus
+ from urllib2 import HTTPBasicAuthHandler, HTTPCookieProcessor, HTTPPasswordMgrWithDefaultRealm, HTTPError
+
+from urlgrabber.grabber import URLGrabber, URLGrabError
+from urlgrabber.mirror import MirrorGroup
+from .core import makeurl, streamfile
+from .util import packagequery, cpio
+from . import conf
+from . import oscerr
+import tempfile
+import re
+try:
+ from .meter import TextMeter
+except:
+ TextMeter = None
+
+
+def join_url(self, base_url, rel_url):
+ """to override _join_url of MirrorGroup, because we want to
+ pass full URLs instead of base URL where relative_url is added later...
+ IOW, we make MirrorGroup ignore relative_url
+ """
+ return base_url
+
+
+class OscFileGrabber(URLGrabber):
+ def __init__(self, progress_obj=None):
+ # we cannot use super because we still have to support
+ # older urlgrabber versions where URLGrabber is an old-style class
+ URLGrabber.__init__(self)
+ self.progress_obj = progress_obj
+
+ def urlgrab(self, url, filename, text=None, **kwargs):
+ if url.startswith('file://'):
+ f = url.replace('file://', '', 1)
+ if os.path.isfile(f):
+ return f
+ else:
+ raise URLGrabError(2, 'Local file \'%s\' does not exist' % f)
+ with file(filename, 'wb') as f:
+ try:
+ for i in streamfile(url, progress_obj=self.progress_obj,
+ text=text):
+ f.write(i)
+ except HTTPError as e:
+ exc = URLGrabError(14, str(e))
+ exc.url = url
+ exc.exception = e
+ exc.code = e.code
+ raise exc
+ except IOError as e:
+ raise URLGrabError(4, str(e))
+ return filename
+
+
+class Fetcher:
+ def __init__(self, cachedir='/tmp', api_host_options={}, urllist=[],
+ http_debug=False, cookiejar=None, offline=False, enable_cpio=True):
+ # set up progress bar callback
+ if sys.stdout.isatty() and TextMeter:
+ self.progress_obj = TextMeter(fo=sys.stdout)
+ else:
+ self.progress_obj = None
+
+ self.cachedir = cachedir
+ self.urllist = urllist
+ self.http_debug = http_debug
+ self.offline = offline
+ self.cpio = {}
+ self.enable_cpio = enable_cpio
+
+ passmgr = HTTPPasswordMgrWithDefaultRealm()
+ for host in api_host_options:
+ passmgr.add_password(None, host, api_host_options[host]['user'],
+ api_host_options[host]['pass'])
+ openers = (HTTPBasicAuthHandler(passmgr), )
+ if cookiejar:
+ openers += (HTTPCookieProcessor(cookiejar), )
+ self.gr = OscFileGrabber(progress_obj=self.progress_obj)
+
+ def failureReport(self, errobj):
+ """failure output for failovers from urlgrabber"""
+ if errobj.url.startswith('file://'):
+ return {}
+ print('%s/%s: attempting download from api, since not found at %s'
+ % (self.curpac.project, self.curpac, errobj.url.split('/')[2]))
+ return {}
+
+ def __add_cpio(self, pac):
+ prpap = '%s/%s/%s/%s' % (pac.project, pac.repository, pac.repoarch, pac.repopackage)
+ self.cpio.setdefault(prpap, {})[pac.repofilename] = pac
+
+ def __download_cpio_archive(self, apiurl, project, repo, arch, package, **pkgs):
+ if not pkgs:
+ return
+ query = ['binary=%s' % quote_plus(i) for i in pkgs]
+ query.append('view=cpio')
+ try:
+ url = makeurl(apiurl, ['build', project, repo, arch, package], query=query)
+ sys.stdout.write("preparing download ...\r")
+ sys.stdout.flush()
+ with tempfile.NamedTemporaryFile(prefix='osc_build_cpio') as tmparchive:
+ self.gr.urlgrab(url, filename=tmparchive.name,
+ text='fetching packages for \'%s\'' % project)
+ archive = cpio.CpioRead(tmparchive.name)
+ archive.read()
+ for hdr in archive:
+ # XXX: we won't have an .errors file because we're using
+ # getbinarylist instead of the public/... route
+ # (which is routed to getbinaries)
+ # getbinaries does not support kiwi builds
+ if hdr.filename == '.errors':
+ archive.copyin_file(hdr.filename)
+ raise oscerr.APIError('CPIO archive is incomplete '
+ '(see .errors file)')
+ if package == '_repository':
+ n = re.sub(r'\.pkg\.tar\..z$', '.arch', hdr.filename)
+ pac = pkgs[n.rsplit('.', 1)[0]]
+ else:
+ # this is a kiwi product
+ pac = pkgs[hdr.filename]
+
+ # Extract a single file from the cpio archive
+ try:
+ fd, tmpfile = tempfile.mkstemp(prefix='osc_build_file')
+ archive.copyin_file(hdr.filename,
+ os.path.dirname(tmpfile),
+ os.path.basename(tmpfile))
+ self.move_package(tmpfile, pac.localdir, pac)
+ finally:
+ os.close(fd)
+ if os.path.exists(tmpfile):
+ os.unlink(tmpfile)
+
+ for pac in pkgs.values():
+ if not os.path.isfile(pac.fullfilename):
+ raise oscerr.APIError('failed to fetch file \'%s\': '
+ 'missing in CPIO archive' %
+ pac.repofilename)
+ except URLGrabError as e:
+ if e.errno != 14 or e.code != 414:
+ raise
+ # query str was too large
+ keys = list(pkgs.keys())
+ if len(keys) == 1:
+ raise oscerr.APIError('unable to fetch cpio archive: '
+ 'server always returns code 414')
+ n = len(pkgs) / 2
+ new_pkgs = dict([(k, pkgs[k]) for k in keys[:n]])
+ self.__download_cpio_archive(apiurl, project, repo, arch,
+ package, **new_pkgs)
+ new_pkgs = dict([(k, pkgs[k]) for k in keys[n:]])
+ self.__download_cpio_archive(apiurl, project, repo, arch,
+ package, **new_pkgs)
+
+ def __fetch_cpio(self, apiurl):
+ for prpap, pkgs in self.cpio.items():
+ project, repo, arch, package = prpap.split('/', 3)
+ self.__download_cpio_archive(apiurl, project, repo, arch, package, **pkgs)
+
+ def fetch(self, pac, prefix=''):
+ # for use by the failure callback
+ self.curpac = pac
+
+ MirrorGroup._join_url = join_url
+ mg = MirrorGroup(self.gr, pac.urllist, failure_callback=(self.failureReport, (), {}))
+
+ if self.http_debug:
+ print('\nURLs to try for package \'%s\':' % pac, file=sys.stderr)
+ print('\n'.join(pac.urllist), file=sys.stderr)
+ print(file=sys.stderr)
+
+ try:
+ with tempfile.NamedTemporaryFile(prefix='osc_build',
+ delete=False) as tmpfile:
+ mg.urlgrab(pac.filename, filename=tmpfile.name,
+ text='%s(%s) %s' % (prefix, pac.project, pac.filename))
+ self.move_package(tmpfile.name, pac.localdir, pac)
+ except URLGrabError as e:
+ if self.enable_cpio and e.errno == 256:
+ self.__add_cpio(pac)
+ return
+ print()
+ print('Error:', e.strerror, file=sys.stderr)
+ print('Failed to retrieve %s from the following locations '
+ '(in order):' % pac.filename, file=sys.stderr)
+ print('\n'.join(pac.urllist), file=sys.stderr)
+ sys.exit(1)
+ finally:
+ if os.path.exists(tmpfile.name):
+ os.unlink(tmpfile.name)
+
+ def move_package(self, tmpfile, destdir, pac_obj=None):
+ import shutil
+ pkgq = packagequery.PackageQuery.query(tmpfile, extra_rpmtags=(1044, 1051, 1052))
+ if pkgq:
+ canonname = pkgq.canonname()
+ else:
+ if pac_obj is None:
+ print('Unsupported file type: ', tmpfile, file=sys.stderr)
+ sys.exit(1)
+ canonname = pac_obj.binary
+
+ fullfilename = os.path.join(destdir, canonname)
+ if pac_obj is not None:
+ pac_obj.canonname = canonname
+ pac_obj.fullfilename = fullfilename
+ shutil.move(tmpfile, fullfilename)
+ os.chmod(fullfilename, 0o644)
+
+ def dirSetup(self, pac):
+ dir = os.path.join(self.cachedir, pac.localdir)
+ if not os.path.exists(dir):
+ try:
+ os.makedirs(dir, mode=0o755)
+ except OSError as e:
+ print('packagecachedir is not writable for you?', file=sys.stderr)
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+ def run(self, buildinfo):
+ cached = 0
+ all = len(buildinfo.deps)
+ for i in buildinfo.deps:
+ i.makeurls(self.cachedir, self.urllist)
+ if os.path.exists(i.fullfilename):
+ cached += 1
+ if i.hdrmd5:
+ from .util import packagequery
+ hdrmd5 = packagequery.PackageQuery.queryhdrmd5(i.fullfilename)
+ if not hdrmd5 or hdrmd5 != i.hdrmd5:
+ os.unlink(i.fullfilename)
+ cached -= 1
+ miss = 0
+ needed = all - cached
+ if all:
+ miss = 100.0 * needed / all
+ print("%.1f%% cache miss. %d/%d dependencies cached.\n" % (miss, cached, all))
+ done = 1
+ for i in buildinfo.deps:
+ i.makeurls(self.cachedir, self.urllist)
+ if not os.path.exists(i.fullfilename):
+ if self.offline:
+ raise oscerr.OscIOError(None,
+ 'Missing \'%s\' in cache: '
+ '--offline not possible.' %
+ i.fullfilename)
+ self.dirSetup(i)
+ if i.hdrmd5 and self.enable_cpio:
+ self.__add_cpio(i)
+ done += 1
+ continue
+ try:
+ # if there isn't a progress bar, there is no output at all
+ if not self.progress_obj:
+ print('%d/%d (%s) %s' % (done, needed, i.project, i.filename))
+ self.fetch(i)
+ if self.progress_obj:
+ print(" %d/%d\r" % (done, needed), end=' ')
+ sys.stdout.flush()
+
+ except KeyboardInterrupt:
+ print('Cancelled by user (ctrl-c)')
+ print('Exiting.')
+ sys.exit(0)
+ done += 1
+
+ self.__fetch_cpio(buildinfo.apiurl)
+
+ prjs = list(buildinfo.projects.keys())
+ for i in prjs:
+ dest = "%s/%s" % (self.cachedir, i)
+ if not os.path.exists(dest):
+ os.makedirs(dest, mode=0o755)
+ dest += '/_pubkey'
+
+ url = makeurl(buildinfo.apiurl, ['source', i, '_pubkey'])
+ try:
+ if self.offline and not os.path.exists(dest):
+ # may need to try parent
+ raise URLGrabError(2)
+ elif not self.offline:
+ OscFileGrabber().urlgrab(url, dest)
+ # not that many keys usually
+ if i not in buildinfo.prjkeys:
+ buildinfo.keys.append(dest)
+ buildinfo.prjkeys.append(i)
+ except KeyboardInterrupt:
+ print('Cancelled by user (ctrl-c)')
+ print('Exiting.')
+ if os.path.exists(dest):
+ os.unlink(dest)
+ sys.exit(0)
+ except URLGrabError as e:
+ # Not found is okay, let's go to the next project
+ if e.errno == 14 and e.code != 404:
+ print("Invalid answer from server", e, file=sys.stderr)
+ sys.exit(1)
+
+ if self.http_debug:
+ print("can't fetch key for %s: %s" % (i, e.strerror), file=sys.stderr)
+ print("url: %s" % url, file=sys.stderr)
+
+ if os.path.exists(dest):
+ os.unlink(dest)
+
+ l = i.rsplit(':', 1)
+ # try key from parent project
+ if len(l) > 1 and l[1] and not l[0] in buildinfo.projects:
+ prjs.append(l[0])
+
+
+def verify_pacs_old(pac_list):
+ """Take a list of rpm filenames and run rpm -K on them.
+
+ In case of failure, exit.
+
+ Check all packages in one go, since this takes only 6 seconds on my Athlon 700
+ instead of 20 when calling 'rpm -K' for each of them.
+ """
+ import subprocess
+
+ if not pac_list:
+ return
+
+ # don't care about the return value because we check the
+ # output anyway, and rpm always writes to stdout.
+
+ # save locale first (we rely on English rpm output here)
+ saved_LC_ALL = os.environ.get('LC_ALL')
+ os.environ['LC_ALL'] = 'en_EN'
+
+ o = subprocess.Popen(['rpm', '-K'] + pac_list, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, close_fds=True).stdout
+
+ # restore locale
+ if saved_LC_ALL:
+ os.environ['LC_ALL'] = saved_LC_ALL
+ else:
+ os.environ.pop('LC_ALL')
+
+ for line in o.readlines():
+
+ if 'OK' not in line:
+ print()
+ print('The following package could not be verified:', file=sys.stderr)
+ print(line, file=sys.stderr)
+ sys.exit(1)
+
+ if 'NOT OK' in line:
+ print()
+ print('The following package could not be verified:', file=sys.stderr)
+ print(line, file=sys.stderr)
+
+ if 'MISSING KEYS' in line:
+ missing_key = line.split('#')[-1].split(')')[0]
+
+ print("""
+- If the key (%(name)s) is missing, install it first.
+ For example, do the following:
+ osc signkey PROJECT > file
+ and, as root:
+ rpm --import %(dir)s/keyfile-%(name)s
+
+ Then, just start the build again.
+
+- If you do not trust the packages, you should configure osc build for XEN or KVM
+
+- You may use --no-verify to skip the verification (which is a risk for your system).
+""" % {'name': missing_key,
+ 'dir': os.path.expanduser('~')}, file=sys.stderr)
+
+ else:
+ print("""
+- If the signature is wrong, you may try deleting the package manually
+ and re-run this program, so it is fetched again.
+""", file=sys.stderr)
+
+ sys.exit(1)
+
+
+def verify_pacs(bi):
+ """Take a list of rpm filenames and verify their signatures.
+
+ In case of failure, exit.
+ """
+
+ pac_list = [i.fullfilename for i in bi.deps]
+ if conf.config['builtin_signature_check'] is not True:
+ return verify_pacs_old(pac_list)
+
+ if not pac_list:
+ return
+
+ if not bi.keys:
+ raise oscerr.APIError("can't verify packages due to lack of GPG keys")
+
+ print("using keys from", ', '.join(bi.prjkeys))
+
+ from . import checker
+ failed = False
+ checker = checker.Checker()
+ try:
+ checker.readkeys(bi.keys)
+ for pkg in pac_list:
+ try:
+ checker.check(pkg)
+ except Exception as e:
+ failed = True
+ print(pkg, ':', e)
+ except:
+ checker.cleanup()
+ raise
+
+ if failed:
+ checker.cleanup()
+ sys.exit(1)
+
+ checker.cleanup()
+
+# vim: sw=4 et
diff --git a/osc/meter.py b/osc/meter.py
new file mode 100644
index 0000000..f24b18d
--- /dev/null
+++ b/osc/meter.py
@@ -0,0 +1,103 @@
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330,
+# Boston, MA 02111-1307 USA
+
+# this is basically a copy of python-urlgrabber's TextMeter class,
+# with support added for dynamical sizing according to screen size.
+# it uses getScreenWidth() scrapped from smart.
+# 2007-04-24, poeml
+
+from __future__ import print_function
+
+from urlgrabber.progress import BaseMeter, format_time, format_number
+import sys, os
+
+def getScreenWidth():
+ import termios, struct, fcntl
+ s = struct.pack('HHHH', 0, 0, 0, 0)
+ try:
+ x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
+ except IOError:
+ return 80
+ return struct.unpack('HHHH', x)[1]
+
+
+class TextMeter(BaseMeter):
+ def __init__(self, fo=sys.stderr, hide_finished=False):
+ BaseMeter.__init__(self)
+ self.fo = fo
+ self.hide_finished = hide_finished
+ try:
+ width = int(os.environ['COLUMNS'])
+ except (KeyError, ValueError):
+ width = getScreenWidth()
+
+
+ #self.unsized_templ = '\r%-60.60s %5sB %s '
+ self.unsized_templ = '\r%%-%s.%ss %%5sB %%s ' % (width *2/5, width*3/5)
+ #self.sized_templ = '\r%-45.45s %3i%% |%-15.15s| %5sB %8s '
+ self.bar_length = width/5
+ self.sized_templ = '\r%%-%s.%ss %%3i%%%% |%%-%s.%ss| %%5sB %%8s ' % (width*4/10, width*4/10, self.bar_length, self.bar_length)
+
+
+ def _do_start(self, *args, **kwargs):
+ BaseMeter._do_start(self, *args, **kwargs)
+ self._do_update(0)
+
+ def _do_update(self, amount_read, now=None):
+ etime = self.re.elapsed_time()
+ fetime = format_time(etime)
+ fread = format_number(amount_read)
+ #self.size = None
+ if self.text is not None:
+ text = self.text
+ else:
+ text = self.basename
+ if self.size is None:
+ out = self.unsized_templ % \
+ (text, fread, fetime)
+ else:
+ rtime = self.re.remaining_time()
+ frtime = format_time(rtime)
+ frac = self.re.fraction_read()
+ bar = '='*int(self.bar_length * frac)
+
+ out = self.sized_templ % \
+ (text, frac*100, bar, fread, frtime) + 'ETA '
+
+ self.fo.write(out)
+ self.fo.flush()
+
+ def _do_end(self, amount_read, now=None):
+ total_time = format_time(self.re.elapsed_time())
+ total_size = format_number(amount_read)
+ if self.text is not None:
+ text = self.text
+ else:
+ text = self.basename
+ if self.size is None:
+ out = self.unsized_templ % \
+ (text, total_size, total_time)
+ else:
+ bar = '=' * self.bar_length
+ out = self.sized_templ % \
+ (text, 100, bar, total_size, total_time) + ' '
+ if self.hide_finished:
+ self.fo.write('\r'+ ' '*len(out) + '\r')
+ else:
+ self.fo.write(out + '\n')
+ self.fo.flush()
+
+# vim: sw=4 et
diff --git a/osc/oscerr.py b/osc/oscerr.py
new file mode 100644
index 0000000..559f80d
--- /dev/null
+++ b/osc/oscerr.py
@@ -0,0 +1,156 @@
+# Copyright (C) 2008 Novell Inc. All rights reserved.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+
+
+class OscBaseError(Exception):
+ def __init__(self, args=()):
+ Exception.__init__(self)
+ self.args = args
+ def __str__(self):
+ return ''.join(self.args)
+
+class UserAbort(OscBaseError):
+ """Exception raised when the user requested abortion"""
+
+class ConfigError(OscBaseError):
+ """Exception raised when there is an error in the config file"""
+ def __init__(self, msg, fname):
+ OscBaseError.__init__(self)
+ self.msg = msg
+ self.file = fname
+
+class ConfigMissingApiurl(ConfigError):
+ """Exception raised when a apiurl does not exist in the config file"""
+ def __init__(self, msg, fname, url):
+ ConfigError.__init__(self, msg, fname)
+ self.url = url
+
+class APIError(OscBaseError):
+ """Exception raised when there is an error in the output from the API"""
+ def __init__(self, msg):
+ OscBaseError.__init__(self)
+ self.msg = msg
+
+class NoConfigfile(OscBaseError):
+ """Exception raised when osc's configfile cannot be found"""
+ def __init__(self, fname, msg):
+ OscBaseError.__init__(self)
+ self.file = fname
+ self.msg = msg
+
+class ExtRuntimeError(OscBaseError):
+ """Exception raised when there is a runtime error of an external tool"""
+ def __init__(self, msg, fname):
+ OscBaseError.__init__(self)
+ self.msg = msg
+ self.file = fname
+
+class ServiceRuntimeError(OscBaseError):
+ """Exception raised when the execution of a source service failed"""
+ def __init__(self, msg):
+ OscBaseError.__init__(self)
+ self.msg = msg
+
+class WrongArgs(OscBaseError):
+ """Exception raised by the cli for wrong arguments usage"""
+
+class WrongOptions(OscBaseError):
+ """Exception raised by the cli for wrong option usage"""
+ #def __str__(self):
+ # s = 'Sorry, wrong options.'
+ # if self.args:
+ # s += '\n' + self.args
+ # return s
+
+class NoWorkingCopy(OscBaseError):
+ """Exception raised when directory is neither a project dir nor a package dir"""
+
+class NotMissing(OscBaseError):
+ """Exception raised when link target should not exist, but it does"""
+
+class WorkingCopyWrongVersion(OscBaseError):
+ """Exception raised when working copy's .osc/_osclib_version doesn't match"""
+
+class WorkingCopyOutdated(OscBaseError):
+ """Exception raised when the working copy is outdated.
+ It takes a tuple with three arguments: path to wc,
+ revision that it has, revision that it should have.
+ """
+ def __str__(self):
+ return ('Working copy \'%s\' is out of date (rev %s vs rev %s).\n'
+ 'Looks as if you need to update it first.' \
+ % (self[0], self[1], self[2]))
+
+class PackageError(OscBaseError):
+ """Base class for all Package related exceptions"""
+ def __init__(self, prj, pac):
+ OscBaseError.__init__(self)
+ self.prj = prj
+ self.pac = pac
+
+class WorkingCopyInconsistent(PackageError):
+ """Exception raised when the working copy is in an inconsistent state"""
+ def __init__(self, prj, pac, dirty_files, msg):
+ PackageError.__init__(self, prj, pac)
+ self.dirty_files = dirty_files
+ self.msg = msg
+
+class LinkExpandError(PackageError):
+ """Exception raised when source link expansion fails"""
+ def __init__(self, prj, pac, msg):
+ PackageError.__init__(self, prj, pac)
+ self.msg = msg
+
+class OscIOError(OscBaseError):
+ def __init__(self, e, msg):
+ OscBaseError.__init__(self)
+ self.e = e
+ self.msg = msg
+
+class PackageNotInstalled(OscBaseError):
+ """
+ Exception raised when a package is not installed on local system
+ """
+ def __init__(self, pkg):
+ OscBaseError.__init__(self, pkg)
+
+ def __str__(self):
+ return 'Package %s is required for this operation' % ''.join(self.args)
+
+class SignalInterrupt(Exception):
+ """Exception raised on SIGTERM and SIGHUP."""
+
+class PackageExists(PackageError):
+ """
+ Exception raised when a local object already exists
+ """
+ def __init__(self, prj, pac, msg):
+ PackageError.__init__(self, prj, pac)
+ self.msg = msg
+
+class PackageMissing(PackageError):
+ """
+ Exception raised when a local object doesn't exist
+ """
+ def __init__(self, prj, pac, msg):
+ PackageError.__init__(self, prj, pac)
+ self.msg = msg
+
+class PackageFileConflict(PackageError):
+ """
+ Exception raised when there's a file conflict.
+ Conflict doesn't mean an unsuccessfull merge in this context.
+ """
+ def __init__(self, prj, pac, file, msg):
+ PackageError.__init__(self, prj, pac)
+ self.file = file
+ self.msg = msg
+
+class PackageInternalError(PackageError):
+ def __init__(self, prj, pac, msg):
+ PackageError.__init__(self, prj, pac)
+ self.msg = msg
+# vim: sw=4 et
diff --git a/osc/oscssl.py b/osc/oscssl.py
new file mode 100644
index 0000000..62f50c4
--- /dev/null
+++ b/osc/oscssl.py
@@ -0,0 +1,372 @@
+# Copyright (C) 2009 Novell Inc.
+# This program is free software; it may be used, copied, modified
+# and distributed under the terms of the GNU General Public Licence,
+# either version 2, or (at your option) any later version.
+
+from __future__ import print_function
+
+import M2Crypto.httpslib
+from M2Crypto.SSL.Checker import SSLVerificationError
+from M2Crypto import m2, SSL
+import M2Crypto.m2urllib2
+import socket
+import sys
+
+try:
+ from urllib.parse import urlparse, splithost, splitport, splittype
+ from urllib.request import addinfourl
+ from http.client import HTTPSConnection
+except ImportError:
+ #python 2.x
+ from urlparse import urlparse
+ from urllib import addinfourl, splithost, splitport, splittype
+ from httplib import HTTPSConnection
+
+from .core import raw_input
+
+class TrustedCertStore:
+ _tmptrusted = {}
+
+ def __init__(self, host, port, app, cert):
+
+ self.cert = cert
+ self.host = host
+ if self.host == None:
+ raise Exception("empty host")
+ if port:
+ self.host += "_%d" % port
+ import os
+ self.dir = os.path.expanduser('~/.config/%s/trusted-certs' % app)
+ self.file = self.dir + '/%s.pem' % self.host
+
+ def is_known(self):
+ if self.host in self._tmptrusted:
+ return True
+
+ import os
+ if os.path.exists(self.file):
+ return True
+ return False
+
+ def is_trusted(self):
+ import os
+ if self.host in self._tmptrusted:
+ cert = self._tmptrusted[self.host]
+ else:
+ if not os.path.exists(self.file):
+ return False
+ from M2Crypto import X509
+ cert = X509.load_cert(self.file)
+ if self.cert.as_pem() == cert.as_pem():
+ return True
+ else:
+ return False
+
+ def trust_tmp(self):
+ self._tmptrusted[self.host] = self.cert
+
+ def trust_always(self):
+ self.trust_tmp()
+ from M2Crypto import X509
+ import os
+ if not os.path.exists(self.dir):
+ os.makedirs(self.dir)
+ self.cert.save_pem(self.file)
+
+
+# verify_cb is called for each error once
+# we only collect the errors and return suceess
+# connection will be aborted later if it needs to
+def verify_cb(ctx, ok, store):
+ if not ctx.verrs:
+ ctx.verrs = ValidationErrors()
+
+ try:
+ if not ok:
+ ctx.verrs.record(store.get_current_cert(), store.get_error(), store.get_error_depth())
+ return 1
+
+ except Exception as e:
+ print(e, file=sys.stderr)
+ return 0
+
+class FailCert:
+ def __init__(self, cert):
+ self.cert = cert
+ self.errs = []
+
+class ValidationErrors:
+
+ def __init__(self):
+ self.chain_ok = True
+ self.cert_ok = True
+ self.failures = {}
+
+ def record(self, cert, err, depth):
+ #print "cert for %s, level %d fail(%d)" % ( cert.get_subject().commonName, depth, err )
+ if depth == 0:
+ self.cert_ok = False
+ else:
+ self.chain_ok = False
+
+ if not depth in self.failures:
+ self.failures[depth] = FailCert(cert)
+ else:
+ if self.failures[depth].cert.get_fingerprint() != cert.get_fingerprint():
+ raise Exception("Certificate changed unexpectedly. This should not happen")
+ self.failures[depth].errs.append(err)
+
+ def show(self, out):
+ for depth in self.failures.keys():
+ cert = self.failures[depth].cert
+ print("*** certificate verify failed at depth %d" % depth, file=out)
+ print("Subject: ", cert.get_subject(), file=out)
+ print("Issuer: ", cert.get_issuer(), file=out)
+ print("Valid: ", cert.get_not_before(), "-", cert.get_not_after(), file=out)
+ print("Fingerprint(MD5): ", cert.get_fingerprint('md5'), file=out)
+ print("Fingerprint(SHA1): ", cert.get_fingerprint('sha1'), file=out)
+
+ for err in self.failures[depth].errs:
+ reason = "Unknown"
+ try:
+ import M2Crypto.Err
+ reason = M2Crypto.Err.get_x509_verify_error(err)
+ except:
+ pass
+ print("Reason:", reason, file=out)
+
+ # check if the encountered errors could be ignored
+ def could_ignore(self):
+ if not 0 in self.failures:
+ return True
+
+ nonfatal_errors = [
+ m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
+ m2.X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN,
+ m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT,
+ m2.X509_V_ERR_CERT_UNTRUSTED,
+ m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE,
+
+ m2.X509_V_ERR_CERT_NOT_YET_VALID,
+ m2.X509_V_ERR_CERT_HAS_EXPIRED,
+ m2.X509_V_OK,
+ ]
+
+ canignore = True
+ for err in self.failures[0].errs:
+ if not err in nonfatal_errors:
+ canignore = False
+ break
+
+ return canignore
+
+class mySSLContext(SSL.Context):
+
+ def __init__(self):
+ SSL.Context.__init__(self, 'sslv23')
+ self.set_options(m2.SSL_OP_NO_SSLv2 | m2.SSL_OP_NO_SSLv3)
+ self.set_cipher_list("ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH")
+ self.set_session_cache_mode(m2.SSL_SESS_CACHE_CLIENT)
+ self.verrs = None
+ #self.set_info_callback() # debug
+ self.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9, callback=lambda ok, store: verify_cb(self, ok, store))
+
+class myHTTPSHandler(M2Crypto.m2urllib2.HTTPSHandler):
+ handler_order = 499
+ saved_session = None
+
+ def __init__(self, *args, **kwargs):
+ self.appname = kwargs.pop('appname', 'generic')
+ M2Crypto.m2urllib2.HTTPSHandler.__init__(self, *args, **kwargs)
+
+ # copied from M2Crypto.m2urllib2.HTTPSHandler
+ # it's sole purpose is to use our myHTTPSHandler/myHTTPSProxyHandler class
+ # ideally the m2urllib2.HTTPSHandler.https_open() method would be split into
+ # "do_open()" and "https_open()" so that we just need to override
+ # the small "https_open()" method...)
+ def https_open(self, req):
+ host = req.get_host()
+ if not host:
+ raise M2Crypto.m2urllib2.URLError('no host given: ' + req.get_full_url())
+
+ # Our change: Check to see if we're using a proxy.
+ # Then create an appropriate ssl-aware connection.
+ full_url = req.get_full_url()
+ target_host = urlparse(full_url)[1]
+
+ if (target_host != host):
+ h = myProxyHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
+ # M2Crypto.ProxyHTTPSConnection.putrequest expects a fullurl
+ selector = full_url
+ else:
+ h = myHTTPSConnection(host = host, appname = self.appname, ssl_context = self.ctx)
+ selector = req.get_selector()
+ # End our change
+ h.set_debuglevel(self._debuglevel)
+ if self.saved_session:
+ h.set_session(self.saved_session)
+
+ headers = dict(req.headers)
+ headers.update(req.unredirected_hdrs)
+ # We want to make an HTTP/1.1 request, but the addinfourl
+ # class isn't prepared to deal with a persistent connection.
+ # It will try to read all remaining data from the socket,
+ # which will block while the server waits for the next request.
+ # So make sure the connection gets closed after the (only)
+ # request.
+ headers["Connection"] = "close"
+ try:
+ h.request(req.get_method(), selector, req.data, headers)
+ s = h.get_session()
+ if s:
+ self.saved_session = s
+ r = h.getresponse()
+ except socket.error as err: # XXX what error?
+ err.filename = full_url
+ raise M2Crypto.m2urllib2.URLError(err)
+
+ # Pick apart the HTTPResponse object to get the addinfourl
+ # object initialized properly.
+
+ # Wrap the HTTPResponse object in socket's file object adapter
+ # for Windows. That adapter calls recv(), so delegate recv()
+ # to read(). This weird wrapping allows the returned object to
+ # have readline() and readlines() methods.
+
+ # XXX It might be better to extract the read buffering code
+ # out of socket._fileobject() and into a base class.
+
+ r.recv = r.read
+ fp = socket._fileobject(r)
+
+ resp = addinfourl(fp, r.msg, req.get_full_url())
+ resp.code = r.status
+ resp.msg = r.reason
+ return resp
+
+class myHTTPSConnection(M2Crypto.httpslib.HTTPSConnection):
+ def __init__(self, *args, **kwargs):
+ self.appname = kwargs.pop('appname', 'generic')
+ M2Crypto.httpslib.HTTPSConnection.__init__(self, *args, **kwargs)
+
+ def connect(self, *args):
+ self.sock = SSL.Connection(self.ssl_ctx)
+ if self.session:
+ self.sock.set_session(self.session)
+ if hasattr(self.sock, 'set_tlsext_host_name'):
+ self.sock.set_tlsext_host_name(self.host)
+ self.sock.connect((self.host, self.port))
+ verify_certificate(self)
+
+ def getHost(self):
+ return self.host
+
+ def getPort(self):
+ return self.port
+
+class myProxyHTTPSConnection(M2Crypto.httpslib.ProxyHTTPSConnection, HTTPSConnection):
+ def __init__(self, *args, **kwargs):
+ self.appname = kwargs.pop('appname', 'generic')
+ M2Crypto.httpslib.ProxyHTTPSConnection.__init__(self, *args, **kwargs)
+
+ def _start_ssl(self):
+ M2Crypto.httpslib.ProxyHTTPSConnection._start_ssl(self)
+ verify_certificate(self)
+
+ def endheaders(self, *args, **kwargs):
+ if self._proxy_auth is None:
+ self._proxy_auth = self._encode_auth()
+ HTTPSConnection.endheaders(self, *args, **kwargs)
+
+ # broken in m2crypto: port needs to be an int
+ def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
+ #putrequest is called before connect, so can interpret url and get
+ #real host/port to be used to make CONNECT request to proxy
+ proto, rest = splittype(url)
+ if proto is None:
+ raise ValueError("unknown URL type: %s" % url)
+ #get host
+ host, rest = splithost(rest)
+ #try to get port
+ host, port = splitport(host)
+ #if port is not defined try to get from proto
+ if port is None:
+ try:
+ port = self._ports[proto]
+ except KeyError:
+ raise ValueError("unknown protocol for: %s" % url)
+ self._real_host = host
+ self._real_port = int(port)
+ M2Crypto.httpslib.HTTPSConnection.putrequest(self, method, url, skip_host, skip_accept_encoding)
+
+ def getHost(self):
+ return self._real_host
+
+ def getPort(self):
+ return self._real_port
+
+def verify_certificate(connection):
+ ctx = connection.sock.ctx
+ verrs = ctx.verrs
+ ctx.verrs = None
+ cert = connection.sock.get_peer_cert()
+ if not cert:
+ connection.close()
+ raise SSLVerificationError("server did not present a certificate")
+
+ # XXX: should be check if the certificate is known anyways?
+ # Maybe it changed to something valid.
+ if not connection.sock.verify_ok():
+
+ tc = TrustedCertStore(connection.getHost(), connection.getPort(), connection.appname, cert)
+
+ if tc.is_known():
+
+ if tc.is_trusted(): # ok, same cert as the stored one
+ return
+ else:
+ print("WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!", file=sys.stderr)
+ print("IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!", file=sys.stderr)
+ print("offending certificate is at '%s'" % tc.file, file=sys.stderr)
+ raise SSLVerificationError("remote host identification has changed")
+
+ # if http_debug is set we redirect sys.stdout to an StringIO
+ # instance in order to do some header filtering (see conf module)
+ # so we have to use the "original" stdout for printing
+ out = getattr(connection, '_orig_stdout', sys.stdout)
+ verrs.show(out)
+
+ print(file=out)
+
+ if not verrs.could_ignore():
+ raise SSLVerificationError("Certificate validation error cannot be ignored")
+
+ if not verrs.chain_ok:
+ print("A certificate in the chain failed verification", file=out)
+ if not verrs.cert_ok:
+ print("The server certificate failed verification", file=out)
+
+ while True:
+ print("""
+Would you like to
+0 - quit (default)
+1 - continue anyways
+2 - trust the server certificate permanently
+9 - review the server certificate
+""", file=out)
+
+ print("Enter choice [0129]: ", end='', file=out)
+ r = raw_input()
+ if not r or r == '0':
+ connection.close()
+ raise SSLVerificationError("Untrusted Certificate")
+ elif r == '1':
+ tc.trust_tmp()
+ return
+ elif r == '2':
+ tc.trust_always()
+ return
+ elif r == '9':
+ print(cert.as_text(), file=out)
+
+# vim: sw=4 et
diff --git a/osc/oscsslexcp.py b/osc/oscsslexcp.py
new file mode 100644
index 0000000..8c6af82
--- /dev/null
+++ b/osc/oscsslexcp.py
@@ -0,0 +1,8 @@
+class NoSecureSSLError(Exception):
+ def __init__(self, msg):
+ Exception.__init__(self)
+ self.msg = msg
+ def __str__(self):
+ return self.msg
+
+# vim: sw=4 et
diff --git a/osc/util/__init__.py b/osc/util/__init__.py
new file mode 100644
index 0000000..74769f9
--- /dev/null
+++ b/osc/util/__init__.py
@@ -0,0 +1 @@
+__all__ = ['ar', 'cpio', 'debquery', 'packagequery', 'rpmquery', 'safewriter']
diff --git a/osc/util/ar.py b/osc/util/ar.py
new file mode 100644
index 0000000..34337a1
--- /dev/null
+++ b/osc/util/ar.py
@@ -0,0 +1,214 @@
+# Copyright 2009 Marcus Huewe <suse-tux@gmx.de>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 2
+# as published by the Free Software Foundation;
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+import stat
+
+#XXX: python 2.7 contains io.StringIO, which needs unicode instead of str
+#therefor try to import old stuff before new one here
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+# workaround for python24
+if not hasattr(os, 'SEEK_SET'):
+ os.SEEK_SET = 0
+
+class ArError(Exception):
+ """Base class for all ar related errors"""
+ def __init__(self, fn, msg):
+ Exception.__init__(self)
+ self.file = fn
+ self.msg = msg
+
+ def __str__(self):
+ return 'ar error: %s' % self.msg
+
+class ArHdr:
+ """Represents an ar header entry"""
+ def __init__(self, fn, date, uid, gid, mode, size, fmag, off):
+ self.file = fn.strip()
+ self.date = date.strip()
+ self.uid = uid.strip()
+ self.gid = gid.strip()
+ self.mode = stat.S_IMODE(int(mode, 8))
+ self.size = int(size)
+ self.fmag = fmag
+ # data section starts at off and ends at off + size
+ self.dataoff = int(off)
+
+ def __str__(self):
+ return '%16s %d' % (self.file, self.size)
+
+class ArFile(StringIO):
+ """Represents a file which resides in the archive"""
+ def __init__(self, fn, uid, gid, mode, buf):
+ StringIO.__init__(self, buf)
+ self.name = fn
+ self.uid = uid
+ self.gid = gid
+ self.mode = mode
+
+ def saveTo(self, dir = None):
+ """
+ writes file to dir/filename if dir isn't specified the current
+ working dir is used. Additionally it tries to set the owner/group
+ and permissions.
+ """
+ if not dir:
+ dir = os.getcwd()
+ fn = os.path.join(dir, self.name)
+ f = open(fn, 'wb')
+ f.write(self.getvalue())
+ f.close()
+ os.chmod(fn, self.mode)
+ uid = self.uid
+ if uid != os.geteuid() or os.geteuid() != 0:
+ uid = -1
+ gid = self.gid
+ if not gid in os.getgroups() or os.getegid() != 0:
+ gid = -1
+ os.chown(fn, uid, gid)
+
+ def __str__(self):
+ return '%s %s %s %s' % (self.name, self.uid,
+ self.gid, self.mode)
+
+class Ar:
+ """
+ Represents an ar archive (only GNU format is supported).
+ Readonly access.
+ """
+ hdr_len = 60
+ hdr_pat = re.compile('^(.{16})(.{12})(.{6})(.{6})(.{8})(.{10})(.{2})', re.DOTALL)
+
+ def __init__(self, fn = None, fh = None):
+ if fn == None and fh == None:
+ raise ArError('either \'fn\' or \'fh\' must be != None')
+ if fh != None:
+ self.__file = fh
+ self.__closefile = False
+ self.filename = fh.name
+ else:
+ # file object: will be closed in __del__()
+ self.__file = None
+ self.__closefile = True
+ self.filename = fn
+ self._init_datastructs()
+
+ def __del__(self):
+ if self.__file and self.__closefile:
+ self.__file.close()
+
+ def _init_datastructs(self):
+ self.hdrs = []
+ self.ext_fnhdr = None
+
+ def _appendHdr(self, hdr):
+ # GNU uses an internal '//' file to store very long filenames
+ if hdr.file.startswith('//'):
+ self.ext_fnhdr = hdr
+ else:
+ self.hdrs.append(hdr)
+
+ def _fixupFilenames(self):
+ """
+ support the GNU approach for very long filenames:
+ every filename which exceeds 16 bytes is stored in the data section of a special file ('//')
+ and the filename in the header of this long file specifies the offset in the special file's
+ data section. The end of such a filename is indicated with a trailing '/'.
+ Another special file is the '/' which contains the symbol lookup table.
+ """
+ for h in self.hdrs:
+ if h.file == '/':
+ continue
+ # remove slashes which are appended by ar
+ h.file = h.file.rstrip('/')
+ if not h.file.startswith('/'):
+ continue
+ # handle long filename
+ off = int(h.file[1:len(h.file)])
+ start = self.ext_fnhdr.dataoff + off
+ self.__file.seek(start, os.SEEK_SET)
+ # XXX: is it safe to read all the data in one chunk? I assume the '//' data section
+ # won't be too large
+ data = self.__file.read(self.ext_fnhdr.size)
+ end = data.find('/')
+ if end != -1:
+ h.file = data[0:end]
+ else:
+ raise ArError('//', 'invalid data section - trailing slash (off: %d)' % start)
+
+ def _get_file(self, hdr):
+ self.__file.seek(hdr.dataoff, os.SEEK_SET)
+ return ArFile(hdr.file, hdr.uid, hdr.gid, hdr.mode,
+ self.__file.read(hdr.size))
+
+ def read(self):
+ """reads in the archive. It tries to use mmap due to performance reasons (in case of large files)"""
+ if not self.__file:
+ import mmap
+ self.__file = open(self.filename, 'rb')
+ try:
+ if sys.platform[:3] != 'win':
+ self.__file = mmap.mmap(self.__file.fileno(), os.path.getsize(self.__file.name), prot=mmap.PROT_READ)
+ else:
+ self.__file = mmap.mmap(self.__file.fileno(), os.path.getsize(self.__file.name))
+ except EnvironmentError as e:
+ if e.errno == 19 or ( hasattr(e, 'winerror') and e.winerror == 5 ):
+ print('cannot use mmap to read the file, falling back to the default io', file=sys.stderr)
+ else:
+ raise e
+ else:
+ self.__file.seek(0, os.SEEK_SET)
+ self._init_datastructs()
+ data = self.__file.read(7)
+ if data != '!<arch>':
+ raise ArError(self.filename, 'no ar archive')
+ pos = 8
+ while (len(data) != 0):
+ self.__file.seek(pos, os.SEEK_SET)
+ data = self.__file.read(self.hdr_len)
+ if not data:
+ break
+ pos += self.hdr_len
+ m = self.hdr_pat.search(data)
+ if not m:
+ raise ArError(self.filename, 'unexpected hdr entry')
+ args = m.groups() + (pos, )
+ hdr = ArHdr(*args)
+ self._appendHdr(hdr)
+ # data blocks are 2 bytes aligned - if they end on an odd
+ # offset ARFMAG[0] will be used for padding (according to the current binutils code)
+ pos += hdr.size + (hdr.size & 1)
+ self._fixupFilenames()
+
+ def get_file(self, fn):
+ for h in self.hdrs:
+ if h.file == fn:
+ return self._get_file(h)
+ return None
+
+ def __iter__(self):
+ for h in self.hdrs:
+ if h.file == '/':
+ continue
+ yield self._get_file(h)
+ raise StopIteration()
diff --git a/osc/util/archquery.py b/osc/util/archquery.py
new file mode 100644
index 0000000..2af87de
--- /dev/null
+++ b/osc/util/archquery.py
@@ -0,0 +1,187 @@
+
+from __future__ import print_function
+
+import os.path
+import re
+import tarfile
+from . import packagequery
+import subprocess
+
+class ArchError(packagequery.PackageError):
+ pass
+
+class ArchQuery(packagequery.PackageQuery, packagequery.PackageQueryResult):
+ def __init__(self, fh):
+ self.__file = fh
+ self.__path = os.path.abspath(fh.name)
+ self.fields = {}
+ #self.magic = None
+ #self.pkgsuffix = 'pkg.tar.gz'
+ self.pkgsuffix = 'arch'
+
+ def read(self, all_tags=True, self_provides=True, *extra_tags):
+ # all_tags and *extra_tags are currently ignored
+ f = open(self.__path, 'rb')
+ #self.magic = f.read(5)
+ #if self.magic == '\375\067zXZ':
+ # self.pkgsuffix = 'pkg.tar.xz'
+ fn = open('/dev/null', 'wb')
+ pipe = subprocess.Popen(['tar', '-O', '-xf', self.__path, '.PKGINFO'], stdout=subprocess.PIPE, stderr=fn).stdout
+ for line in pipe.readlines():
+ line = line.rstrip().split(' = ', 2)
+ if len(line) == 2:
+ if not line[0] in self.fields:
+ self.fields[line[0]] = []
+ self.fields[line[0]].append(line[1])
+ if self_provides:
+ prv = '%s = %s' % (self.name(), self.fields['pkgver'][0])
+ self.fields.setdefault('provides', []).append(prv)
+ return self
+
+ def vercmp(self, archq):
+ res = cmp(int(self.epoch()), int(archq.epoch()))
+ if res != 0:
+ return res
+ res = ArchQuery.rpmvercmp(self.version(), archq.version())
+ if res != None:
+ return res
+ res = ArchQuery.rpmvercmp(self.release(), archq.release())
+ return res
+
+ def name(self):
+ return self.fields['pkgname'][0] if 'pkgname' in self.fields else None
+
+ def version(self):
+ pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
+ if pkgver != None:
+ pkgver = re.sub(r'[0-9]+:', '', pkgver, 1)
+ pkgver = re.sub(r'-[^-]*$', '', pkgver)
+ return pkgver
+
+ def release(self):
+ pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
+ if pkgver != None:
+ m = re.search(r'-([^-])*$', pkgver)
+ if m:
+ return m.group(1)
+ return None
+
+ def epoch(self):
+ pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
+ if pkgver != None:
+ m = re.match(r'([0-9])+:', pkgver)
+ if m:
+ return m.group(1)
+ return None
+
+ def arch(self):
+ return self.fields['arch'][0] if 'arch' in self.fields else None
+
+ def description(self):
+ return self.fields['pkgdesc'][0] if 'pkgdesc' in self.fields else None
+
+ def path(self):
+ return self.__path
+
+ def provides(self):
+ return self.fields['provides'] if 'provides' in self.fields else []
+
+ def requires(self):
+ return self.fields['depend'] if 'depend' in self.fields else []
+
+ def conflicts(self):
+ return self.fields['conflict'] if 'conflict' in self.fields else []
+
+ def obsoletes(self):
+ return self.fields['replaces'] if 'replaces' in self.fields else []
+
+ def canonname(self):
+ pkgver = self.fields['pkgver'][0] if 'pkgver' in self.fields else None
+ return self.name() + '-' + pkgver + '-' + self.arch() + '.' + self.pkgsuffix
+
+ def gettag(self, tag):
+ # implement me, if needed
+ return None
+
+ @staticmethod
+ def query(filename, all_tags = False, *extra_tags):
+ f = open(filename, 'rb')
+ archq = ArchQuery(f)
+ archq.read(all_tags, *extra_tags)
+ f.close()
+ return archq
+
+ @staticmethod
+ def rpmvercmp(ver1, ver2):
+ """
+ implementation of RPM's version comparison algorithm
+ (as described in lib/rpmvercmp.c)
+ """
+ if ver1 == ver2:
+ return 0
+ res = 0
+ while res == 0:
+ # remove all leading non alphanumeric chars
+ ver1 = re.sub('^[^a-zA-Z0-9]*', '', ver1)
+ ver2 = re.sub('^[^a-zA-Z0-9]*', '', ver2)
+ if not (len(ver1) and len(ver2)):
+ break
+ # check if we have a digits segment
+ mo1 = re.match('(\d+)', ver1)
+ mo2 = re.match('(\d+)', ver2)
+ numeric = True
+ if mo1 is None:
+ mo1 = re.match('([a-zA-Z]+)', ver1)
+ mo2 = re.match('([a-zA-Z]+)', ver2)
+ numeric = False
+ # check for different types: alpha and numeric
+ if mo2 is None:
+ if numeric:
+ return 1
+ return -1
+ seg1 = mo1.group(0)
+ ver1 = ver1[mo1.end(0):]
+ seg2 = mo2.group(1)
+ ver2 = ver2[mo2.end(1):]
+ if numeric:
+ # remove leading zeros
+ seg1 = re.sub('^0+', '', seg1)
+ seg2 = re.sub('^0+', '', seg2)
+ # longer digit segment wins - if both have the same length
+ # a simple ascii compare decides
+ res = len(seg1) - len(seg2) or cmp(seg1, seg2)
+ else:
+ res = cmp(seg1, seg2)
+ if res > 0:
+ return 1
+ elif res < 0:
+ return -1
+ return cmp(ver1, ver2)
+
+ @staticmethod
+ def filename(name, epoch, version, release, arch):
+ if epoch:
+ if release:
+ return '%s-%s:%s-%s-%s.arch' % (name, epoch, version, release, arch)
+ else:
+ return '%s-%s:%s-%s.arch' % (name, epoch, version, arch)
+ if release:
+ return '%s-%s-%s-%s.arch' % (name, version, release, arch)
+ else:
+ return '%s-%s-%s.arch' % (name, version, arch)
+
+
+if __name__ == '__main__':
+ import sys
+ try:
+ archq = ArchQuery.query(sys.argv[1])
+ except ArchError as e:
+ print(e.msg)
+ sys.exit(2)
+ print(archq.name(), archq.version(), archq.release(), archq.arch())
+ print(archq.canonname())
+ print(archq.description())
+ print('##########')
+ print('\n'.join(archq.provides()))
+ print('##########')
+ print('\n'.join(archq.requires()))
diff --git a/osc/util/cpio.py b/osc/util/cpio.py
new file mode 100644
index 0000000..cd3c406
--- /dev/null
+++ b/osc/util/cpio.py
@@ -0,0 +1,256 @@
+# Copyright 2009 Marcus Huewe <suse-tux@gmx.de>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 2
+# as published by the Free Software Foundation;
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+
+from __future__ import print_function
+
+import mmap
+import os
+import stat
+import struct
+import sys
+
+# workaround for python24
+if not hasattr(os, 'SEEK_SET'):
+ os.SEEK_SET = 0
+
+# format implementation is based on src/copyin.c and src/util.c (see cpio sources)
+
+class CpioError(Exception):
+ """base class for all cpio related errors"""
+ def __init__(self, fn, msg):
+ Exception.__init__(self)
+ self.file = fn
+ self.msg = msg
+ def __str__(self):
+ return '%s: %s' % (self.file, self.msg)
+
+class CpioHdr:
+ """
+ Represents a cpio header ("New" portable format and CRC format).
+ """
+ def __init__(self, mgc, ino, mode, uid, gid, nlink, mtime, filesize,
+ dev_maj, dev_min, rdev_maj, rdev_min, namesize, checksum,
+ off = -1, filename = ''):
+ """
+ All passed parameters are hexadecimal strings (not NUL terminated) except
+ off and filename. They will be converted into normal ints.
+ """
+ self.ino = ino
+ self.mode = mode
+ self.uid = uid
+ self.gid = gid
+ self.nlink = nlink
+ self.mtime = mtime
+ # 0 indicates FIFO or dir
+ self.filesize = filesize
+ self.dev_maj = dev_maj
+ self.dev_min = dev_min
+ # only needed for special block/char files
+ self.rdev_maj = rdev_maj
+ self.rdev_min = rdev_min
+ # length of filename (inluding terminating NUL)
+ self.namesize = namesize
+ # != 0 indicates CRC format (which we do not support atm)
+ self.checksum = checksum
+ for k, v in self.__dict__.items():
+ self.__dict__[k] = int(v, 16)
+ self.filename = filename
+ # data starts at dataoff and ends at dataoff+filesize
+ self.dataoff = off
+
+ def __str__(self):
+ return "%s %s %s %s" % (self.filename, self.filesize, self.namesize, self.dataoff)
+
+class CpioRead:
+ """
+ Represents a cpio archive.
+ Supported formats:
+ * ascii SVR4 no CRC also called "new_ascii"
+ """
+
+ # supported formats - use name -> mgc mapping to increase readabilty
+ sfmt = {
+ 'newascii': '070701',
+ }
+
+ # header format
+ hdr_fmt = '6s8s8s8s8s8s8s8s8s8s8s8s8s8s'
+ hdr_len = 110
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.format = -1
+ self.__file = None
+ self._init_datastructs()
+
+ def __del__(self):
+ if self.__file:
+ self.__file.close()
+
+ def __iter__(self):
+ for h in self.hdrs:
+ yield h
+
+ def _init_datastructs(self):
+ self.hdrs = []
+
+ def _calc_padding(self, off):
+ """
+ skip some bytes after a header or a file.
+ based on 'static void tape_skip_padding()' in copyin.c.
+ """
+ if self._is_format('newascii'):
+ return (4 - (off % 4)) % 4
+
+ def _is_format(self, type):
+ return self.format == self.sfmt[type]
+
+ def _copyin_file(self, hdr, dest, fn):
+ """saves file to disk"""
+ # TODO: investigate links (e.g. symbolic links are working)
+ # check if we have a regular file
+ if not stat.S_ISREG(stat.S_IFMT(hdr.mode)):
+ msg = '\'%s\' is no regular file - only regular files are supported atm' % hdr.filename
+ raise NotImplementedError(msg)
+ fn = os.path.join(dest, fn)
+ f = open(fn, 'wb')
+ self.__file.seek(hdr.dataoff, os.SEEK_SET)
+ f.write(self.__file.read(hdr.filesize))
+ f.close()
+ os.chmod(fn, hdr.mode)
+ uid = hdr.uid
+ if uid != os.geteuid() or os.geteuid() != 1:
+ uid = -1
+ gid = hdr.gid
+ if not gid in os.getgroups() or os.getegid() != -1:
+ gid = -1
+ os.chown(fn, uid, gid)
+
+ def _get_hdr(self, fn):
+ for h in self.hdrs:
+ if h.filename == fn:
+ return h
+ return None
+
+ def read(self):
+ if not self.__file:
+ self.__file = open(self.filename, 'rb')
+ try:
+ if sys.platform[:3] != 'win':
+ self.__file = mmap.mmap(self.__file.fileno(), os.path.getsize(self.__file.name), prot = mmap.PROT_READ)
+ else:
+ self.__file = mmap.mmap(self.__file.fileno(), os.path.getsize(self.__file.name))
+ except EnvironmentError as e:
+ if e.errno == 19 or ( hasattr(e, 'winerror') and e.winerror == 5 ):
+ print('cannot use mmap to read the file, failing back to default', file=sys.stderr)
+ else:
+ raise e
+ else:
+ self.__file.seek(0, os.SEEK_SET)
+ self._init_datastructs()
+ data = self.__file.read(6)
+ self.format = data
+ if not self.format in self.sfmt.values():
+ raise CpioError(self.filename, '\'%s\' is not a supported cpio format' % self.format)
+ pos = 0
+ while (len(data) != 0):
+ self.__file.seek(pos, os.SEEK_SET)
+ data = self.__file.read(self.hdr_len)
+ if not data:
+ break
+ pos += self.hdr_len
+ data = struct.unpack(self.hdr_fmt, data)
+ hdr = CpioHdr(*data)
+ hdr.filename = self.__file.read(hdr.namesize - 1)
+ if hdr.filename == 'TRAILER!!!':
+ break
+ pos += hdr.namesize
+ if self._is_format('newascii'):
+ pos += self._calc_padding(hdr.namesize + 110)
+ hdr.dataoff = pos
+ self.hdrs.append(hdr)
+ pos += hdr.filesize + self._calc_padding(hdr.filesize)
+
+ def copyin_file(self, filename, dest = None, new_fn = None):
+ """
+ copies filename to dest.
+ If dest is None the file will be stored in $PWD/filename. If dest points
+ to a dir the file will be stored in dest/filename. In case new_fn is specified
+ the file will be stored as new_fn.
+ """
+ hdr = self._get_hdr(filename)
+ if not hdr:
+ raise CpioError(filename, '\'%s\' does not exist in archive' % filename)
+ dest = dest or os.getcwd()
+ fn = new_fn or filename
+ self._copyin_file(hdr, dest, fn)
+
+ def copyin(self, dest = None):
+ """
+ extracts the cpio archive to dest.
+ If dest is None $PWD will be used.
+ """
+ dest = dest or os.getcwd()
+ for h in self.hdrs:
+ self._copyin_file(h, dest, h.filename)
+
+class CpioWrite:
+ """cpio archive small files in memory, using new style portable header format"""
+
+ def __init__(self):
+ self.cpio = ''
+
+ def add(self, name=None, content=None, perms=0x1a4, type=0x8000):
+ namesize = len(name) + 1
+ if namesize % 2:
+ name += '\0'
+ filesize = len(content)
+ mode = perms | type
+
+ c = []
+ c.append('070701') # magic
+ c.append('%08X' % 0) # inode
+ c.append('%08X' % mode) # mode
+ c.append('%08X' % 0) # uid
+ c.append('%08X' % 0) # gid
+ c.append('%08X' % 0) # nlink
+ c.append('%08X' % 0) # mtime
+ c.append('%08X' % filesize)
+ c.append('%08X' % 0) # major
+ c.append('%08X' % 0) # minor
+ c.append('%08X' % 0) # rmajor
+ c.append('%08X' % 0) # rminor
+ c.append('%08X' % namesize)
+ c.append('%08X' % 0) # checksum
+
+ c.append(name + '\0')
+ c.append('\0' * (len(''.join(c)) % 4))
+
+ c.append(content)
+
+ c = ''.join(c)
+ if len(c) % 4:
+ c += '\0' * (4 - len(c) % 4)
+
+ self.cpio += c
+
+ def add_padding(self):
+ if len(self.cpio) % 512:
+ self.cpio += '\0' * (512 - len(self.cpio) % 512)
+
+ def get(self):
+ self.add('TRAILER!!!', '')
+ self.add_padding()
+ return ''.join(self.cpio)
diff --git a/osc/util/debquery.py b/osc/util/debquery.py
new file mode 100644
index 0000000..4b1a823
--- /dev/null
+++ b/osc/util/debquery.py
@@ -0,0 +1,191 @@
+
+from __future__ import print_function
+
+from . import ar
+import os.path
+import re
+import tarfile
+from . import packagequery
+
+class DebError(packagequery.PackageError):
+ pass
+
+class DebQuery(packagequery.PackageQuery, packagequery.PackageQueryResult):
+
+ default_tags = ('package', 'version', 'release', 'epoch', 'architecture', 'description',
+ 'provides', 'depends', 'pre_depends', 'conflicts', 'breaks')
+
+ def __init__(self, fh):
+ self.__file = fh
+ self.__path = os.path.abspath(fh.name)
+ self.filename_suffix = 'deb'
+ self.fields = {}
+
+ def read(self, all_tags=False, self_provides=True, *extra_tags):
+ arfile = ar.Ar(fh = self.__file)
+ arfile.read()
+ debbin = arfile.get_file('debian-binary')
+ if debbin is None:
+ raise DebError(self.__path, 'no debian binary')
+ if debbin.read() != '2.0\n':
+ raise DebError(self.__path, 'invalid debian binary format')
+ control = arfile.get_file('control.tar.gz')
+ if control is None:
+ raise DebError(self.__path, 'missing control.tar.gz')
+ # XXX: python2.4 relies on a name
+ tar = tarfile.open(name = 'control.tar.gz', fileobj = control)
+ try:
+ name = './control'
+ # workaround for python2.4's tarfile module
+ if 'control' in tar.getnames():
+ name = 'control'
+ control = tar.extractfile(name)
+ except KeyError:
+ raise DebError(self.__path, 'missing \'control\' file in control.tar.gz')
+ self.__parse_control(control, all_tags, self_provides, *extra_tags)
+ return self
+
+ def __parse_control(self, control, all_tags=False, self_provides=True, *extra_tags):
+ data = control.readline().strip()
+ while data:
+ field, val = re.split(':\s*', data.strip(), 1)
+ data = control.readline()
+ while data and re.match('\s+', data):
+ val += '\n' + data.strip()
+ data = control.readline().rstrip()
+ field = field.replace('-', '_').lower()
+ if field in self.default_tags + extra_tags or all_tags:
+ # a hyphen is not allowed in dict keys
+ self.fields[field] = val
+ versrel = self.fields['version'].rsplit('-', 1)
+ if len(versrel) == 2:
+ self.fields['version'] = versrel[0]
+ self.fields['release'] = versrel[1]
+ else:
+ self.fields['release'] = None
+ verep = self.fields['version'].split(':', 1)
+ if len(verep) == 2:
+ self.fields['epoch'] = verep[0]
+ self.fields['version'] = verep[1]
+ else:
+ self.fields['epoch'] = '0'
+ self.fields['provides'] = [ i.strip() for i in re.split(',\s*', self.fields.get('provides', '')) if i ]
+ self.fields['depends'] = [ i.strip() for i in re.split(',\s*', self.fields.get('depends', '')) if i ]
+ self.fields['pre_depends'] = [ i.strip() for i in re.split(',\s*', self.fields.get('pre_depends', '')) if i ]
+ self.fields['conflicts'] = [ i.strip() for i in re.split(',\s*', self.fields.get('conflicts', '')) if i ]
+ self.fields['breaks'] = [ i.strip() for i in re.split(',\s*', self.fields.get('breaks', '')) if i ]
+ if self_provides:
+ # add self provides entry
+ self.fields['provides'].append('%s (= %s)' % (self.name(), '-'.join(versrel)))
+
+ def vercmp(self, debq):
+ res = cmp(int(self.epoch()), int(debq.epoch()))
+ if res != 0:
+ return res
+ res = DebQuery.debvercmp(self.version(), debq.version())
+ if res != None:
+ return res
+ res = DebQuery.debvercmp(self.release(), debq.release())
+ return res
+
+ def name(self):
+ return self.fields['package']
+
+ def version(self):
+ return self.fields['version']
+
+ def release(self):
+ return self.fields['release']
+
+ def epoch(self):
+ return self.fields['epoch']
+
+ def arch(self):
+ return self.fields['architecture']
+
+ def description(self):
+ return self.fields['description']
+
+ def path(self):
+ return self.__path
+
+ def provides(self):
+ return self.fields['provides']
+
+ def requires(self):
+ return self.fields['depends'] + self.fields['pre_depends']
+
+ def conflicts(self):
+ return self.fields['conflicts'] + self.fields['breaks']
+
+ def obsoletes(self):
+ return []
+
+ def gettag(self, num):
+ return self.fields.get(num, None)
+
+ def canonname(self):
+ return DebQuery.filename(self.name(), self.epoch(), self.version(), self.release(), self.arch())
+
+ @staticmethod
+ def query(filename, all_tags = False, *extra_tags):
+ f = open(filename, 'rb')
+ debq = DebQuery(f)
+ debq.read(all_tags, *extra_tags)
+ f.close()
+ return debq
+
+ @staticmethod
+ def debvercmp(ver1, ver2):
+ """
+ implementation of dpkg's version comparison algorithm
+ """
+ # 32 is arbitrary - it is needed for the "longer digit string wins" handling
+ # (found this nice approach in Build/Deb.pm (build package))
+ ver1 = re.sub('(\d+)', lambda m: (32 * '0' + m.group(1))[-32:], ver1)
+ ver2 = re.sub('(\d+)', lambda m: (32 * '0' + m.group(1))[-32:], ver2)
+ vers = map(lambda x, y: (x or '', y or ''), ver1, ver2)
+ for v1, v2 in vers:
+ if v1 == v2:
+ continue
+ if (v1.isalpha() and v2.isalpha()) or (v1.isdigit() and v2.isdigit()):
+ res = cmp(v1, v2)
+ if res != 0:
+ return res
+ else:
+ if v1 == '~' or not v1:
+ return -1
+ elif v2 == '~' or not v2:
+ return 1
+ ord1 = ord(v1)
+ if not (v1.isalpha() or v1.isdigit()):
+ ord1 += 256
+ ord2 = ord(v2)
+ if not (v2.isalpha() or v2.isdigit()):
+ ord2 += 256
+ if ord1 > ord2:
+ return 1
+ else:
+ return -1
+ return 0
+
+ @staticmethod
+ def filename(name, epoch, version, release, arch):
+ if release:
+ return '%s_%s-%s_%s.deb' % (name, version, release, arch)
+ else:
+ return '%s_%s_%s.deb' % (name, version, arch)
+
+if __name__ == '__main__':
+ import sys
+ try:
+ debq = DebQuery.query(sys.argv[1])
+ except DebError as e:
+ print(e.msg)
+ sys.exit(2)
+ print(debq.name(), debq.version(), debq.release(), debq.arch())
+ print(debq.description())
+ print('##########')
+ print('\n'.join(debq.provides()))
+ print('##########')
+ print('\n'.join(debq.requires()))
diff --git a/osc/util/packagequery.py b/osc/util/packagequery.py
new file mode 100644
index 0000000..950f222
--- /dev/null
+++ b/osc/util/packagequery.py
@@ -0,0 +1,164 @@
+
+from __future__ import print_function
+
+class PackageError(Exception):
+ """base class for all package related errors"""
+ def __init__(self, fname, msg):
+ Exception.__init__(self)
+ self.fname = fname
+ self.msg = msg
+
+class PackageQueries(dict):
+ """Dict of package name keys and package query values. When assigning a
+ package query, to a name, the package is evaluated to see if it matches the
+ wanted architecture and if it has a greater version than the current value.
+ """
+
+ # map debian arches to common obs arches
+ architectureMap = {'i386': ['i586', 'i686'], 'amd64': ['x86_64'], 'ppc64el': ['ppc64le']}
+
+ def __init__(self, wanted_architecture):
+ self.wanted_architecture = wanted_architecture
+ super(PackageQueries, self).__init__()
+
+ def add(self, query):
+ """Adds package query to dict if it is of the correct architecture and
+ is newer (has a greater version) than the currently assigned package.
+
+ @param a PackageQuery
+ """
+ self.__setitem__(query.name(), query)
+
+ def __setitem__(self, name, query):
+ if name != query.name():
+ raise ValueError("key '%s' does not match "
+ "package query name '%s'" % (name, query.name()))
+
+ architecture = query.arch()
+
+ if (architecture in [self.wanted_architecture, 'noarch', 'all', 'any']
+ or self.wanted_architecture in self.architectureMap.get(architecture,
+ [])):
+ current_query = self.get(name)
+
+ # if current query does not exist or is older than this new query
+ if current_query is None or current_query.vercmp(query) <= 0:
+ super(PackageQueries, self).__setitem__(name, query)
+
+class PackageQuery:
+ """abstract base class for all package types"""
+ def read(self, all_tags = False, *extra_tags):
+ """Returns a PackageQueryResult instance"""
+ raise NotImplementedError
+
+ # Hmmm... this should be a module function (inherting this stuff
+ # does not make much sense) (the same is true for the queryhdrmd5 method)
+ @staticmethod
+ def query(filename, all_tags=False, extra_rpmtags=(), extra_debtags=(), self_provides=True):
+ f = open(filename, 'rb')
+ magic = f.read(7)
+ f.seek(0)
+ extra_tags = ()
+ pkgquery = None
+ if magic[:4] == '\xed\xab\xee\xdb':
+ from . import rpmquery
+ pkgquery = rpmquery.RpmQuery(f)
+ extra_tags = extra_rpmtags
+ elif magic == '!<arch>':
+ from . import debquery
+ pkgquery = debquery.DebQuery(f)
+ extra_tags = extra_debtags
+ elif magic[:5] == '<?xml':
+ f.close()
+ return None
+ elif magic[:5] == '\375\067zXZ' or magic[:2] == '\037\213':
+ from . import archquery
+ pkgquery = archquery.ArchQuery(f)
+ else:
+ raise PackageError(filename, 'unsupported package type. magic: \'%s\'' % magic)
+ pkgqueryresult = pkgquery.read(all_tags, self_provides, *extra_tags)
+ f.close()
+ return pkgqueryresult
+
+ @staticmethod
+ def queryhdrmd5(filename):
+ f = open(filename, 'rb')
+ magic = f.read(7)
+ f.seek(0)
+ if magic[:4] == '\xed\xab\xee\xdb':
+ from . import rpmquery
+ f.close()
+ return rpmquery.RpmQuery.queryhdrmd5(filename)
+ return None
+
+
+class PackageQueryResult:
+ """abstract base class that represents the result of a package query"""
+ def name(self):
+ raise NotImplementedError
+
+ def version(self):
+ raise NotImplementedError
+
+ def release(self):
+ raise NotImplementedError
+
+ def epoch(self):
+ raise NotImplementedError
+
+ def arch(self):
+ raise NotImplementedError
+
+ def description(self):
+ raise NotImplementedError
+
+ def path(self):
+ raise NotImplementedError
+
+ def provides(self):
+ raise NotImplementedError
+
+ def requires(self):
+ raise NotImplementedError
+
+ def conflicts(self):
+ raise NotImplementedError
+
+ def obsoletes(self):
+ raise NotImplementedError
+
+ def gettag(self, tag):
+ raise NotImplementedError
+
+ def vercmp(self, pkgquery):
+ raise NotImplementedError
+
+ def canonname(self):
+ raise NotImplementedError
+
+ def evr(self):
+ evr = self.version()
+
+ if self.release():
+ evr += "-" + self.release()
+
+ epoch = self.epoch()
+ if epoch is not None and epoch != 0:
+ evr = epoch + ":" + evr
+ return evr
+
+if __name__ == '__main__':
+ import sys
+ try:
+ pkgq = PackageQuery.query(sys.argv[1])
+ except PackageError as e:
+ print(e.msg)
+ sys.exit(2)
+ print(pkgq.name())
+ print(pkgq.version())
+ print(pkgq.release())
+ print(pkgq.description())
+ print('##########')
+ print('\n'.join(pkgq.provides()))
+ print('##########')
+ print('\n'.join(pkgq.requires()))
diff --git a/osc/util/repodata.py b/osc/util/repodata.py
new file mode 100644
index 0000000..7f952f5
--- /dev/null
+++ b/osc/util/repodata.py
@@ -0,0 +1,176 @@
+"""Module for reading repodata directory (created with createrepo) for package
+information instead of scanning individual rpms."""
+
+# standard modules
+import gzip
+import os.path
+
+# cElementTree can be standard or 3rd-party depending on python version
+try:
+ from xml.etree import cElementTree as ET
+except ImportError:
+ import cElementTree as ET
+
+# project modules
+import osc.util.rpmquery
+import osc.util.packagequery
+
+def namespace(name):
+ return "{http://linux.duke.edu/metadata/%s}" % name
+
+OPERATOR_BY_FLAGS = {
+ "EQ" : "=",
+ "LE" : "<=",
+ "GE" : ">="
+}
+
+def primaryPath(directory):
+ """Returns path to the primary repository data file.
+
+ @param directory repository directory that contains the repodata subdirectory
+ @return str path to primary repository data file
+ @raise IOError if repomd.xml contains no primary location
+ """
+ metaDataPath = os.path.join(directory, "repodata", "repomd.xml")
+ elementTree = ET.parse(metaDataPath)
+ root = elementTree.getroot()
+
+ for dataElement in root:
+ if dataElement.get("type") == "primary":
+ locationElement = dataElement.find(namespace("repo") + "location")
+ # even though the repomd.xml file is under repodata, the location a
+ # attribute is relative to parent directory (directory).
+ primaryPath = os.path.join(directory, locationElement.get("href"))
+ break
+ else:
+ raise IOError("'%s' contains no primary location" % metaDataPath)
+
+ return primaryPath
+
+def queries(directory):
+ """Returns a list of RepoDataQueries constructed from the repodata under
+ the directory.
+
+ @param directory path to a repository directory (parent directory of
+ repodata directory)
+ @return list of RepoDataQueryResult instances
+ @raise IOError if repomd.xml contains no primary location
+ """
+ path = primaryPath(directory)
+
+ gunzippedPrimary = gzip.GzipFile(path)
+ elementTree = ET.parse(gunzippedPrimary)
+ root = elementTree.getroot()
+
+ packageQueries = []
+ for packageElement in root:
+ packageQuery = RepoDataQueryResult(directory, packageElement)
+ packageQueries.append(packageQuery)
+
+ return packageQueries
+
+class RepoDataQueryResult(osc.util.packagequery.PackageQueryResult):
+ """PackageQueryResult that reads in data from the repodata directory files."""
+
+ def __init__(self, directory, element):
+ """Creates a RepoDataQueryResult from the a package Element under a metadata
+ Element in a primary.xml file.
+
+ @param directory repository directory path. Used to convert relative
+ paths to full paths.
+ @param element package Element
+ """
+ self.__directory = os.path.abspath(directory)
+ self.__element = element
+
+ def __formatElement(self):
+ return self.__element.find(namespace("common") + "format")
+
+ def __parseEntry(self, element):
+ entry = element.get("name")
+ flags = element.get("flags")
+
+ if flags is not None:
+ version = element.get("ver")
+ operator = OPERATOR_BY_FLAGS[flags]
+ entry += " %s %s" % (operator, version)
+
+ release = element.get("rel")
+ if release is not None:
+ entry += "-%s" % release
+
+ return entry
+
+ def __parseEntryCollection(self, collection):
+ formatElement = self.__formatElement()
+ collectionElement = formatElement.find(namespace("rpm") + collection)
+
+ entries = []
+ if collectionElement is not None:
+ for entryElement in collectionElement.findall(namespace("rpm") +
+ "entry"):
+ entry = self.__parseEntry(entryElement)
+ entries.append(entry)
+
+ return entries
+
+ def __versionElement(self):
+ return self.__element.find(namespace("common") + "version")
+
+ def arch(self):
+ return self.__element.find(namespace("common") + "arch").text
+
+ def description(self):
+ return self.__element.find(namespace("common") + "description").text
+
+ def distribution(self):
+ return None
+
+ def epoch(self):
+ return self.__versionElement().get("epoch")
+
+ def name(self):
+ return self.__element.find(namespace("common") + "name").text
+
+ def path(self):
+ locationElement = self.__element.find(namespace("common") + "location")
+ relativePath = locationElement.get("href")
+ absolutePath = os.path.join(self.__directory, relativePath)
+
+ return absolutePath
+
+ def provides(self):
+ return self.__parseEntryCollection("provides")
+
+ def release(self):
+ return self.__versionElement().get("rel")
+
+ def requires(self):
+ return self.__parseEntryCollection("requires")
+
+ def conflicts(self):
+ return self.__parseEntryCollection('conflicts')
+
+ def obsoletes(self):
+ return self.__parseEntryCollection('obsoletes')
+
+ def canonname(self):
+ return osc.util.rpmquery.RpmQuery.filename(self.name(), None,
+ self.version(), self.release(), self.arch())
+
+ def gettag(self, tag):
+ # implement me, if needed
+ return None
+
+ def vercmp(self, other):
+ res = osc.util.rpmquery.RpmQuery.rpmvercmp(str(self.epoch()), str(other.epoch()))
+ if res != 0:
+ return res
+ res = osc.util.rpmquery.RpmQuery.rpmvercmp(self.version(), other.version())
+ if res != 0:
+ return res
+ res = osc.util.rpmquery.RpmQuery.rpmvercmp(self.release(), other.release())
+ return res
+
+ def version(self):
+ return self.__versionElement().get("ver")
diff --git a/osc/util/rpmquery.py b/osc/util/rpmquery.py
new file mode 100644
index 0000000..de05ff5
--- /dev/null
+++ b/osc/util/rpmquery.py
@@ -0,0 +1,366 @@
+
+from __future__ import print_function
+
+import os
+import re
+import struct
+from . import packagequery
+
+class RpmError(packagequery.PackageError):
+ pass
+
+class RpmHeaderError(RpmError):
+ pass
+
+class RpmHeader:
+ """corresponds more or less to the indexEntry_s struct"""
+ def __init__(self, offset, length):
+ self.offset = offset
+ # length of the data section (without length of indexEntries)
+ self.length = length
+ self.entries = []
+
+ def append(self, entry):
+ self.entries.append(entry)
+
+ def gettag(self, tag):
+ for i in self.entries:
+ if i.tag == tag:
+ return i
+ return None
+
+ def __iter__(self):
+ for i in self.entries:
+ yield i
+
+ def __len__(self):
+ return len(self.entries)
+
+class RpmHeaderEntry:
+ """corresponds to the entryInfo_s struct (except the data attribute)"""
+
+ # each element represents an int
+ ENTRY_SIZE = 16
+ def __init__(self, tag, type, offset, count):
+ self.tag = tag
+ self.type = type
+ self.offset = offset
+ self.count = count
+ self.data = None
+
+class RpmQuery(packagequery.PackageQuery, packagequery.PackageQueryResult):
+ LEAD_SIZE = 96
+ LEAD_MAGIC = 0xedabeedb
+ HEADER_MAGIC = 0x8eade801
+ HEADERSIG_TYPE = 5
+
+ LESS = 1 << 1
+ GREATER = 1 << 2
+ EQUAL = 1 << 3
+
+ default_tags = (1000, 1001, 1002, 1003, 1004, 1022, 1005, 1020,
+ 1047, 1112, 1113, # provides
+ 1049, 1048, 1050, # requires
+ 1054, 1053, 1055, # conflicts
+ 1090, 1114, 1115 # obsoletes
+ )
+
+ def __init__(self, fh):
+ self.__file = fh
+ self.__path = os.path.abspath(fh.name)
+ self.filename_suffix = 'rpm'
+ self.header = None
+
+ def read(self, all_tags=False, self_provides=True, *extra_tags, **extra_kw):
+ # self_provides is unused because a rpm always has a self provides
+ self.__read_lead()
+ data = self.__file.read(RpmHeaderEntry.ENTRY_SIZE)
+ hdrmgc, reserved, il, dl = struct.unpack('!I3i', data)
+ if self.HEADER_MAGIC != hdrmgc:
+ raise RpmHeaderError(self.__path, 'invalid headermagic \'%s\'' % hdrmgc)
+ # skip signature header for now
+ size = il * RpmHeaderEntry.ENTRY_SIZE + dl
+ # data is 8 byte aligned
+ pad = (size + 7) & ~7
+ querysig = extra_kw.get('querysig')
+ if not querysig:
+ self.__file.read(pad)
+ data = self.__file.read(RpmHeaderEntry.ENTRY_SIZE)
+ hdrmgc, reserved, il, dl = struct.unpack('!I3i', data)
+ self.header = RpmHeader(pad, dl)
+ if self.HEADER_MAGIC != hdrmgc:
+ raise RpmHeaderError(self.__path, 'invalid headermagic \'%s\'' % hdrmgc)
+ data = self.__file.read(il * RpmHeaderEntry.ENTRY_SIZE)
+ while len(data) > 0:
+ ei = struct.unpack('!4i', data[:RpmHeaderEntry.ENTRY_SIZE])
+ self.header.append(RpmHeaderEntry(*ei))
+ data = data[RpmHeaderEntry.ENTRY_SIZE:]
+ data = self.__file.read(self.header.length)
+ for i in self.header:
+ if i.tag in self.default_tags + extra_tags or all_tags:
+ try: # this may fail for -debug* packages
+ self.__read_data(i, data)
+ except: pass
+ return self
+
+ def __read_lead(self):
+ data = self.__file.read(self.LEAD_SIZE)
+ leadmgc, = struct.unpack('!I', data[:4])
+ if leadmgc != self.LEAD_MAGIC:
+ raise RpmError(self.__path, 'invalid lead magic \'%s\'' % leadmgc)
+ sigtype, = struct.unpack('!h', data[78:80])
+ if sigtype != self.HEADERSIG_TYPE:
+ raise RpmError(self.__path, 'invalid header signature \'%s\'' % sigtype)
+
+ def __read_data(self, entry, data):
+ off = entry.offset
+ if entry.type == 2:
+ entry.data = struct.unpack('!%dc' % entry.count, data[off:off + 1 * entry.count])
+ if entry.type == 3:
+ entry.data = struct.unpack('!%dh' % entry.count, data[off:off + 2 * entry.count])
+ elif entry.type == 4:
+ entry.data = struct.unpack('!%di' % entry.count, data[off:off + 4 * entry.count])
+ elif entry.type == 6:
+ entry.data = unpack_string(data[off:])
+ elif entry.type == 7:
+ entry.data = data[off:off + entry.count]
+ elif entry.type == 8 or entry.type == 9:
+ cnt = entry.count
+ entry.data = []
+ while cnt > 0:
+ cnt -= 1
+ s = unpack_string(data[off:])
+ # also skip '\0'
+ off += len(s) + 1
+ entry.data.append(s)
+ if entry.type == 8:
+ return
+ lang = os.getenv('LANGUAGE') or os.getenv('LC_ALL') \
+ or os.getenv('LC_MESSAGES') or os.getenv('LANG')
+ if lang is None:
+ entry.data = entry.data[0]
+ return
+ # get private i18n table
+ table = self.header.gettag(100)
+ # just care about the country code
+ lang = lang.split('_', 1)[0]
+ cnt = 0
+ for i in table.data:
+ if cnt > len(entry.data) - 1:
+ break
+ if i == lang:
+ entry.data = entry.data[cnt]
+ return
+ cnt += 1
+ entry.data = entry.data[0]
+ else:
+ raise RpmHeaderError(self.__path, 'unsupported tag type \'%d\' (tag: \'%s\'' % (entry.type, entry.tag))
+
+ def __reqprov(self, tag, flags, version):
+ pnames = self.header.gettag(tag)
+ if not pnames:
+ return []
+ pnames = pnames.data
+ pflags = self.header.gettag(flags).data
+ pvers = self.header.gettag(version).data
+ if not (pnames and pflags and pvers):
+ raise RpmError(self.__path, 'cannot get provides/requires, tags are missing')
+ res = []
+ for name, flags, ver in zip(pnames, pflags, pvers):
+ # RPMSENSE_SENSEMASK = 15 (see rpmlib.h) but ignore RPMSENSE_SERIAL (= 1 << 0) therefore use 14
+ if flags & 14:
+ name += ' '
+ if flags & self.GREATER:
+ name += '>'
+ elif flags & self.LESS:
+ name += '<'
+ if flags & self.EQUAL:
+ name += '='
+ name += ' %s' % ver
+ res.append(name)
+ return res
+
+ def vercmp(self, rpmq):
+ res = RpmQuery.rpmvercmp(str(self.epoch()), str(rpmq.epoch()))
+ if res != 0:
+ return res
+ res = RpmQuery.rpmvercmp(self.version(), rpmq.version())
+ if res != 0:
+ return res
+ res = RpmQuery.rpmvercmp(self.release(), rpmq.release())
+ return res
+
+ # XXX: create dict for the tag => number mapping?!
+ def name(self):
+ return self.header.gettag(1000).data
+
+ def version(self):
+ return self.header.gettag(1001).data
+
+ def release(self):
+ return self.header.gettag(1002).data
+
+ def epoch(self):
+ epoch = self.header.gettag(1003)
+ if epoch is None:
+ return 0
+ return epoch.data[0]
+
+ def arch(self):
+ return self.header.gettag(1022).data
+
+ def summary(self):
+ return self.header.gettag(1004).data
+
+ def description(self):
+ return self.header.gettag(1005).data
+
+ def url(self):
+ entry = self.header.gettag(1020)
+ if entry is None:
+ return None
+ return entry.data
+
+ def path(self):
+ return self.__path
+
+ def provides(self):
+ return self.__reqprov(1047, 1112, 1113)
+
+ def requires(self):
+ return self.__reqprov(1049, 1048, 1050)
+
+ def conflicts(self):
+ return self.__reqprov(1054, 1053, 1055)
+
+ def obsoletes(self):
+ return self.__reqprov(1090, 1114, 1115)
+
+ def is_src(self):
+ # SOURCERPM = 1044
+ return self.gettag(1044) is None
+
+ def is_nosrc(self):
+ # NOSOURCE = 1051, NOPATCH = 1052
+ return self.is_src() and \
+ (self.gettag(1051) is not None or self.gettag(1052) is not None)
+
+ def gettag(self, num):
+ return self.header.gettag(num)
+
+ def canonname(self):
+ if self.is_nosrc():
+ arch = 'nosrc'
+ elif self.is_src():
+ arch = 'src'
+ else:
+ arch = self.arch()
+ return RpmQuery.filename(self.name(), None, self.version(), self.release(), arch)
+
+ @staticmethod
+ def query(filename):
+ f = open(filename, 'rb')
+ rpmq = RpmQuery(f)
+ rpmq.read()
+ f.close()
+ return rpmq
+
+ @staticmethod
+ def queryhdrmd5(filename):
+ f = open(filename, 'rb')
+ rpmq = RpmQuery(f)
+ rpmq.read(1004, querysig=True)
+ f.close()
+ entry = rpmq.gettag(1004)
+ if entry is None:
+ return None
+ return ''.join([ "%02x" % x for x in struct.unpack('16B', entry.data) ])
+
+ @staticmethod
+ def rpmvercmp(ver1, ver2):
+ """
+ implementation of RPM's version comparison algorithm
+ (as described in lib/rpmvercmp.c)
+ """
+ if ver1 == ver2:
+ return 0
+ res = 0
+ while res == 0:
+ # remove all leading non alphanumeric or tilde chars
+ ver1 = re.sub('^[^a-zA-Z0-9~]*', '', ver1)
+ ver2 = re.sub('^[^a-zA-Z0-9~]*', '', ver2)
+ if ver1.startswith('~') or ver2.startswith('~'):
+ if not ver1.startswith('~'):
+ return 1
+ elif not ver2.startswith('~'):
+ return -1
+ ver1 = ver1[1:]
+ ver2 = ver2[1:]
+ continue
+
+ if not (len(ver1) and len(ver2)):
+ break
+
+ # check if we have a digits segment
+ mo1 = re.match('(\d+)', ver1)
+ mo2 = re.match('(\d+)', ver2)
+ numeric = True
+ if mo1 is None:
+ mo1 = re.match('([a-zA-Z]+)', ver1)
+ mo2 = re.match('([a-zA-Z]+)', ver2)
+ numeric = False
+ # check for different types: alpha and numeric
+ if mo2 is None:
+ if numeric:
+ return 1
+ return -1
+ seg1 = mo1.group(0)
+ ver1 = ver1[mo1.end(0):]
+ seg2 = mo2.group(1)
+ ver2 = ver2[mo2.end(1):]
+ if numeric:
+ # remove leading zeros
+ seg1 = re.sub('^0+', '', seg1)
+ seg2 = re.sub('^0+', '', seg2)
+ # longer digit segment wins - if both have the same length
+ # a simple ascii compare decides
+ res = len(seg1) - len(seg2) or cmp(seg1, seg2)
+ else:
+ res = cmp(seg1, seg2)
+ if res > 0:
+ return 1
+ elif res < 0:
+ return -1
+ return cmp(ver1, ver2)
+
+ @staticmethod
+ def filename(name, epoch, version, release, arch):
+ return '%s-%s-%s.%s.rpm' % (name, version, release, arch)
+
+def unpack_string(data):
+ """unpack a '\\0' terminated string from data"""
+ val = ''
+ for c in data:
+ c, = struct.unpack('!c', c)
+ if c == '\0':
+ break
+ else:
+ val += c
+ return val
+
+if __name__ == '__main__':
+ import sys
+ try:
+ rpmq = RpmQuery.query(sys.argv[1])
+ except RpmError as e:
+ print(e.msg)
+ sys.exit(2)
+ print(rpmq.name(), rpmq.version(), rpmq.release(), rpmq.arch(), rpmq.url())
+ print(rpmq.summary())
+ print(rpmq.description())
+ print('##########')
+ print('\n'.join(rpmq.provides()))
+ print('##########')
+ print('\n'.join(rpmq.requires()))
+ print('##########')
+ print(RpmQuery.queryhdrmd5(sys.argv[1]))
diff --git a/osc/util/safewriter.py b/osc/util/safewriter.py
new file mode 100644
index 0000000..079e3d8
--- /dev/null
+++ b/osc/util/safewriter.py
@@ -0,0 +1,29 @@
+# be careful when debugging this code:
+# don't add print statements when setting sys.stdout = SafeWriter(sys.stdout)...
+class SafeWriter:
+ """
+ Safely write an (unicode) str. In case of an "UnicodeEncodeError" the
+ the str is encoded with the "encoding" encoding.
+ All getattr, setattr calls are passed through to the "writer" instance.
+ """
+ def __init__(self, writer, encoding='unicode_escape'):
+ self.__dict__['writer'] = writer
+ self.__dict__['encoding'] = encoding
+
+ def __get_writer(self):
+ return self.__dict__['writer']
+
+ def __get_encoding(self):
+ return self.__dict__['encoding']
+
+ def write(self, s):
+ try:
+ self.__get_writer().write(s)
+ except UnicodeEncodeError as e:
+ self.__get_writer().write(s.encode(self.__get_encoding()))
+
+ def __getattr__(self, name):
+ return getattr(self.__get_writer(), name)
+
+ def __setattr__(self, name, value):
+ setattr(self.__get_writer(), name, value)
diff --git a/osc_expand_link.pl b/osc_expand_link.pl
new file mode 100755
index 0000000..7a46c07
--- /dev/null
+++ b/osc_expand_link.pl
@@ -0,0 +1,491 @@
+#! /usr/bin/perl -w
+#
+# osc_expand_link.pl -- a tool to help osc build packages where an _link exists.
+# (C) 2006 jw@suse.de, distribute under GPL v2.
+#
+# 2006-12-12, jw
+# 2006-12-15, jw, v0.2 -- {files}{error} gets printed if present.
+# 2008-03-25, jw, v0.3 -- go via api using iChains and ~/.oscrc
+# 2008-03-26, jw, v0.4 -- added linked file retrieval and usage.
+# 2009-10-21, jw, added obsolete warning, in favour of osc co -e
+
+use Data::Dumper;
+use LWP::UserAgent;
+use HTTP::Status;
+use Digest::MD5;
+
+my $version = '0.4';
+my $verbose = 1;
+
+print "This $0 is obsolete. Please use instead: osc co -e\n";
+sleep 5;
+
+# curl buildservice:5352/source/home:jnweiger/vim
+# curl 'buildservice:5352/source/home:jnweiger/vim?rev=d90bfab4301f758e0d82cf09aa263d37'
+# curl 'buildservice:5352/source/home:jnweiger/vim/vim.spec?rev=d90bfab4301f758e0d82cf09aa263d37'
+
+my $cfg = {
+ apiurl => slurp_file(".osc/_apiurl", 1),
+ package => slurp_file(".osc/_package", 1),
+ project => slurp_file(".osc/_project", 1),
+ files => xml_slurp_file(".osc/_files", { container => 'directory', attr => 'merge' }),
+ link => xml_slurp_file(".osc/_link", { container => 'link', attr => 'merge' }),
+};
+
+{
+ package CredUserAgent;
+ @ISA = qw(LWP::UserAgent);
+
+ sub new
+ {
+ my $self = LWP::UserAgent::new(@_);
+ $self->agent("osc_expand_link.pl/$version");
+ $self;
+ }
+ sub get_basic_credentials
+ {
+ my ($self, $realm, $uri) = @_;
+ my $netloc = $uri->host_port;
+
+ unless ($self->{auth})
+ {
+ print STDERR "Auth for $realm at $netloc\n";
+ unless (open IN, "<", "$ENV{HOME}/.oscrc")
+ {
+ print STDERR "$ENV{HOME}/.oscrc: $!\n";
+ return (undef, undef);
+ }
+ while (defined (my $line = <IN>))
+ {
+ chomp $line;
+ $self->{auth}{pass} = $1 if $line =~ m{^pass\s*=\s*(\S+)};
+ $self->{auth}{user} = $1 if $line =~ m{^user\s*=\s*(\S+)};
+ }
+ close IN;
+ print STDERR "~/.oscrc: user=$self->{auth}{user}\n";
+ }
+ return ($self->{auth}{user},$self->{auth}{pass});
+ }
+}
+
+my $ua = CredUserAgent->new (keep_alive => 1);
+
+sub cred_get
+{
+ my ($url) = @_;
+ my $r = $ua->get($url);
+ die "$url: " . $r->status_line . "\n" unless $r->is_success;
+ return $r->content;
+}
+
+sub cred_getstore
+{
+ my ($url, $file) = @_;
+ my $r = $ua->get($url, ':content_file' => $file);
+ die "$url: " . $r->status_line . "\n" unless $r->is_success;
+ $r->code;
+}
+
+$cfg->{apiurl} ||= 'https://api.opensuse.org';
+$cfg->{project} ||= '<Project>';
+$cfg->{package} ||= '<Package>';
+
+chomp $cfg->{apiurl};
+chomp $cfg->{project};
+chomp $cfg->{package};
+
+my $source = "$cfg->{apiurl}/source";
+my $url = "$source/$cfg->{project}/$cfg->{package}";
+
+if (my $url = $ARGV[0])
+ {
+
+ die qq{osc_expand_link $version;
+
+Usage:
+
+ osc co $cfg->{project} $cfg->{package}
+ cd $cfg->{project}/$cfg->{package}
+ $0
+
+to resolve a _link.
+
+or
+
+ $0 $cfg->{apiurl}/source/$cfg->{project}/$cfg->{package}
+
+to review internal buildservice data.
+
+or
+ $0 $cfg->{apiurl}/source/$cfg->{project}/$cfg->{package}/linked/\\*.spec
+
+ cd $cfg->{project}/$cfg->{package}
+ $0 linked \\*.spec
+
+to retrieve the original specfile behind a link.
+
+} if $url =~ m{^-};
+
+ $url = "$url/$ARGV[1]" if $url eq 'linked' and $ARGV[1];
+ if ($url =~ m{^(.*/)?linked/(.*)$})
+ {
+ $url = (defined $1) ? $1 : "$cfg->{project}/$cfg->{package}";
+ my $file = $2;
+ $url = "$source/$url" if $cfg->{apiurl} and $url !~ m{://};
+ print STDERR "$url\n";
+ my $dir = xml_parse(cred_get($url), 'merge');
+ my $li = $dir->{directory}{linkinfo} || die "no linkinfo in $url\n";
+ $url = "$source/$li->{project}/$li->{package}";
+ mkdir("linked");
+
+ if ($file =~ m{\*})
+ {
+ my $dir = xml_parse(cred_get($url), 'merge');
+ $dir = $dir->{directory} if $dir->{directory};
+ my @list = sort map { $_->{name} } @{$dir->{entry}};
+ my $file_re = "\Q$file\E"; $file_re =~ s{\\\*}{\.\*}g;
+ my @match = grep { $_ =~ m{^$file_re$} } @list;
+ die "pattern $file not found in\n @list\n" unless @match;
+ $file = $match[0];
+ }
+ $url .= "/$file";
+
+ print STDERR "$url -> linked/$file\n";
+ my $r = cred_getstore($url, "linked/$file");
+ print STDERR " Error: $r\n" if $r != RC_OK;
+ exit 0;
+ }
+
+ $url = "$cfg->{project}/$cfg->{package}/$url" unless $url =~ m{/};
+ $url = "$source/$url" if $cfg->{apiurl} and $url !~ m{://};
+ print cred_get($url);
+ exit 0;
+ }
+
+warn "$cfg->{project}/$cfg->{package} error: $cfg->{files}{error}\n" if $cfg->{files}{error};
+die "$cfg->{project}/$cfg->{package} has no _link\n" unless $cfg->{link};
+die "$cfg->{project}/$cfg->{package} has no xsrcmd5\n" unless $cfg->{files}{xsrcmd5};
+
+print STDERR "expanding link to $cfg->{link}{project}/$cfg->{link}{package}\n";
+if (my $p = $cfg->{link}{patches})
+ {
+ $p = [ $p ] if ref $p ne 'ARRAY';
+ my @p = map { "$_->{apply}{name}" } @$p;
+ print STDERR "applied patches: " . join(',', @p) . "\n";
+ }
+
+my $dir = xml_parse(cred_get("$url?rev=$cfg->{files}{xsrcmd5}"), 'merge');
+$dir = $dir->{directory} if defined $dir->{directory};
+$dir->{entry} = [ $dir->{entry} ] if ref $dir->{entry} ne 'ARRAY';
+for my $file (@{$dir->{entry}})
+ {
+ if (-f $file->{name})
+ {
+ ## check the md5sum of the existing file and be happy.
+ $md5 = Digest::MD5->new;
+ open IN, "<", $file->{name} or die "md5sum($file->{name} failed: $!";
+ $md5->addfile(*IN);
+ close IN;
+ if ($md5->hexdigest eq $file->{md5})
+ {
+ print STDERR " - $file->{name} (md5 unchanged)\n";
+ }
+ else
+ {
+ print STDERR "Modified: $file->{name}, please commit changes!\n";
+ }
+ next;
+ }
+ print STDERR " get $file->{name}";
+ # fixme: xsrcmd5 is obsolete.
+ # use <linkinfo project="openSUSE:Factory" package="avrdude" xsrcmd5="a39c2bd14c3ad5dbb82edd7909fcdfc4">
+ my $response = cred_getstore("$url/$file->{name}?rev=$cfg->{files}{xsrcmd5}", $file->{name});
+ print STDERR ($response == RC_OK) ? "\n" : " Error:$response\n";
+ }
+exit 0;
+##########################################################################
+
+sub slurp_file
+{
+ my ($path, $silent) = @_;
+ open IN, "<", $path or ($silent ? return undef : die "slurp_file($path) failed: $!\n");
+ my $body = join '', <IN>;
+ close IN;
+ return $body;
+}
+
+
+#################################################################
+## xml parser imported from w3dcm.pl and somewhat expanded.
+## 2006-12-15, jw
+##
+## xml_parse assumes correct container closing.
+## Any </...> tag would closes an open <foo>.
+## Thus xml_parse is not suitable for HTML.
+##
+sub xml_parse
+{
+ my ($text, $attr) = @_;
+ my %xml;
+ my @stack = ();
+ my $t = \%xml;
+
+#print "xml_parse: '$text'\n";
+ my @tags = find_tags($text);
+ for my $i (0 .. $#tags)
+ {
+ my $tag = substr $text, $tags[$i]->{offset}, $tags[$i]->{tag_len};
+ my $cdata = '';
+ my $s = $tags[$i]->{offset} + $tags[$i]->{tag_len};
+ if (defined $tags[$i+1])
+ {
+ my $l = $tags[$i+1]->{offset} - $s;
+ $cdata = substr $text, $s, $l;
+ }
+ else
+ {
+ $cdata = substr $text, $s;
+ }
+
+# print "tag=$tag\n";
+ my $name = $1 if $tag =~ s{<([\?/]?[\w:-]+)\s*}{};
+ $tag =~ s{>\s*$}{};
+ my $nest = ($tag =~ s{[\?/]$}{}) ? 0 : 1;
+ my $close = ($name =~ s{^/}{}) ? 1 : 0;
+# print "name=$name, attr='$tag', $close, $nest, '$cdata'\n";
+
+ my $x = {};
+ $x->{-cdata} .= $cdata if $nest;
+ xml_add_attr($x, $tag, $attr) unless $tag eq '';
+
+ if (!$close)
+ {
+ delete $t->{-cdata} if $t->{-cdata} and $t->{-cdata} =~ m{^\s*$};
+ unless ($t->{$name})
+ {
+ $t->{$name} = $x;
+ }
+ else
+ {
+ $t->{$name} = [ $t->{$name} ] unless ref $t->{$name} eq 'ARRAY';
+ push @{$t->{$name}}, $x;
+ }
+ }
+
+
+ if ($close)
+ {
+ $t = pop @stack;
+ }
+ elsif ($nest)
+ {
+ push @stack, $t;
+ $t = $x;
+ }
+ }
+
+ print "stack=", Data::Dumper::Dumper(\@stack) if $verbose > 2;
+ scalar_cdata($t);
+ return $t;
+}
+
+##
+## reads a file formatted by xml_make, and returns a hash.
+## The toplevel container is removed from that hash, if specified.
+## A wildcard '*' can be specified to remove any toplevel container.
+## Otherwise the name of the container must match precisely.
+##
+sub xml_slurp_file
+{
+ my ($file, $opt) = @_;
+ unless (open IN, "<$file")
+ {
+ return undef unless $opt->{die};
+ die "xml_slurp($opt->{container}): cannot read $file: $!\n";
+ }
+
+ my $xml = join '', <IN>; close IN;
+ $xml = xml_parse($xml, $opt->{attr});
+ if (my $container = $opt->{container})
+ {
+ die "xml_slurp($file, '$container') malformed file, should have only one toplevel node.\n"
+ unless scalar keys %$xml == 1;
+ $container = (keys %$xml)[0] if $container eq '' or $container eq '*';
+ die "xml_slurp($file, '$container') toplevel tag missing or wrong.\n" unless $xml->{$container};
+ $xml = $xml->{$container};
+ }
+ return $xml;
+}
+
+sub xml_escape
+{
+ my ($text) = @_;
+
+ ## XML::Simple does just that:
+ $text =~ s{&}{&amp;}g;
+ $text =~ s{<}{&lt;}g;
+ $text =~ s{>}{&gt;}g;
+ $text =~ s{"}{&quot;}g;
+ return $text;
+}
+
+sub xml_unescape
+{
+ my ($text) = @_;
+
+ ## XX: Fimxe: we should handle some more escapes here...
+ ## and better do it in a single pass.
+ $text =~ s{&#([\d]{3});}{chr $1}eg;
+ $text =~ s{&lt;}{<}g;
+ $text =~ s{&gt;}{>}g;
+ $text =~ s{&quot;}{"}g;
+ $text =~ s{&amp;}{&}g;
+
+ return $text;
+}
+
+##
+## find all hashes, that contain exactly one key named '-cdata'
+## and replace these hashes with the value of that key.
+## These values are scalar when created by xml_parse(), hence the name.
+##
+sub scalar_cdata
+{
+ my ($hash) = @_;
+ my $selftag = '.scalar_cdata_running';
+
+ return unless ref $hash eq 'HASH';
+ return if $hash->{$selftag};
+ $hash->{$selftag} = 1;
+
+ for my $key (keys %$hash)
+ {
+ my $val = $hash->{$key};
+ if (ref $val eq 'ARRAY')
+ {
+ for my $i (0..$#$val)
+ {
+ scalar_cdata($hash->{$key}[$i]);
+ }
+ }
+ elsif (ref $val eq 'HASH')
+ {
+ my @k = keys %$val;
+ if (scalar(@k) == 1 && ($k[0] eq '-cdata'))
+ {
+ $hash->{$key} = $hash->{$key}{-cdata};
+ }
+ else
+ {
+ delete $hash->{$key}{-cdata} if exists $val->{-cdata} && $val->{-cdata} =~ m{^\s*$};
+ scalar_cdata($hash->{$key});
+ }
+ }
+ }
+ delete $hash->{$selftag};
+}
+
+##
+## find_tags -- a brute force tag finder.
+## This code is robust enough to parse the weirdest HTML.
+## An Array containing hashes of { offset, name, tag_len } is returned.
+## CDATA is skipped, but can be determined from gaps between tags.
+## The name parser may chop names, so XML-style tag names are
+## unreliable.
+##
+sub find_tags
+{
+ my ($text) = @_;
+ my $last = '';
+ my @tags;
+ my $inquotes = 0;
+ my $incomment = 0;
+
+ while ($text =~ m{(<!--|-->|"|>|<)(/?\w*)}g)
+ {
+ my ($offset, $what, $name) = (length $`, $1, $2);
+
+ if ($inquotes)
+ {
+ $inquotes = 0 if $what eq '"';
+ next;
+ }
+
+ if ($incomment)
+ {
+ $incomment = 0 if $what eq '-->';
+ next;
+ }
+
+ if ($what eq '"')
+ {
+ $inquotes = 1;
+ next;
+ }
+
+ if ($what eq '<!--')
+ {
+ $incomment = 1;
+ next;
+ }
+
+ next if $what eq $last; # opening and closing angular brackets are polar.
+
+ if ($what eq '>' and scalar @tags)
+ {
+ $tags[$#tags]{tag_len} = 1 + $offset - $tags[$#tags]{offset};
+ }
+
+ if ($what eq '<')
+ {
+ push @tags, {name => $name, offset => $offset };
+ }
+
+ $last = $what;
+ }
+ return @tags;
+}
+
+##
+## how = undef: defaults to '-attr plain'
+## how = '-attr plain': add the attributes as one scalar value to hash-element -attr
+## how = '-attr hash': add the attributes as a hash-ref to hash-element -attr
+## how = 'merge': add the attributes as direct hash elements. (This is irreversible)
+##
+## attributes are either space-separated, or delimited with '' or "".
+sub xml_add_attr
+{
+ my ($hash, $text, $how) = @_;
+ $how = 'plain' unless $how;
+ my $tag = '-attr'; $tag = $1 if $how =~ s{^\s*([\w_:-]+)\s+(.*)$}{$2};
+ $how = lc $how;
+
+ return $hash->{$tag} = $text if $how eq 'plain';
+
+ if ($how eq 'hash')
+ {
+ $hash = $hash->{$tag} = {};
+ $how = 'merge';
+ ## fallthrough
+ }
+ if ($how eq 'merge')
+ {
+ while ($text =~ m{([\w_:-]+)\s*=("[^"]*"|'[^']'|\S*)\s*}g)
+ {
+ my ($key, $val) = ($1, $2);
+ $val =~ s{^"(.*)"$}{$1} unless $val =~ s{^'(.*)'$}{$1};
+ if (defined($hash->{$key}))
+ {
+ ## redefinition. promote to array and push.
+ $hash->{$key} = [ $hash->{$key} ] unless ref $hash->{$key};
+ push @{$hash->{$key}}, $val;
+ }
+ else
+ {
+ $hash->{$key} = $val;
+ }
+ }
+ return $hash;
+ }
+ die "xml_expand_attr: unknown method '$how'\n";
+}
diff --git a/osc_hotshot.py b/osc_hotshot.py
new file mode 100755
index 0000000..54ac40f
--- /dev/null
+++ b/osc_hotshot.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+import hotshot, hotshot.stats
+import tempfile
+import os, sys
+
+from osc import commandline
+
+
+if __name__ == '__main__':
+
+ (fd, filename) = tempfile.mkstemp(prefix = 'osc_profiledata_', dir = '/dev/shm')
+ f = os.fdopen(fd)
+
+ try:
+
+ prof = hotshot.Profile(filename)
+
+ prof.runcall(commandline.main)
+ print 'run complete. analyzing.'
+ prof.close()
+
+ stats = hotshot.stats.load(filename)
+ stats.strip_dirs()
+ stats.sort_stats('time', 'calls')
+ stats.print_stats(20)
+
+ del stats
+
+ finally:
+ f.close()
+ os.unlink(filename)
diff --git a/run_bandit.sh b/run_bandit.sh
new file mode 100755
index 0000000..a743dee
--- /dev/null
+++ b/run_bandit.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# you can pass as argument "csv","json" or "txt" (default)
+if [ "$1" != "" ];then
+ OUTPUT=$1
+else
+ OUTPUT="txt"
+fi
+
+# check if bandit is installed
+command -v bandit >/dev/null 2>&1 || { echo "bandit should be installed. get the package from https://build.opensuse.org/package/show/home:vpereirabr/python-bandit. Aborting." >&2; exit 1; }
+
+bandit -c /usr/etc/bandit/bandit.yaml -r osc -f $OUTPUT
+
+if [ "$OUTPUT" == "csv" ];then
+ cat bandit_results.csv
+ rm -f bandit_results.csv
+fi
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..1aaca6b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+import distutils.command.build
+import distutils.command.install_data
+import os.path
+import osc.core
+import sys
+from osc import commandline
+from osc import babysitter
+# optional support for py2exe
+try:
+ import py2exe
+ HAVE_PY2EXE = True
+except:
+ HAVE_PY2EXE = False
+
+
+class build_osc(distutils.command.build.build, object):
+ """
+ Custom build command which generates man page.
+ """
+
+ def build_man_page(self):
+ """
+ """
+ import gzip
+ man_path = os.path.join('build', 'osc.1.gz')
+ distutils.log.info('generating %s' % man_path)
+ outfile = gzip.open(man_path, 'w')
+ osccli = commandline.Osc(stdout=outfile)
+ # FIXME: we cannot call the main method because osc expects an ~/.oscrc
+ # file (this would break builds in environments like the obs)
+ #osccli.main(argv = ['osc','man'])
+ osccli.optparser = osccli.get_optparser()
+ osccli.do_man(None)
+ outfile.close()
+
+ def run(self):
+ super(build_osc, self).run()
+ self.build_man_page()
+
+
+# Support for documentation (sphinx)
+class build_docs(distutils.command.build.Command):
+ description = 'builds documentation using sphinx'
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ # metadata contains information supplied in setup()
+ metadata = self.distribution.metadata
+ # package_dir may be None, in that case use the current directory.
+ src_dir = (self.distribution.package_dir or {'': ''})['']
+ src_dir = os.path.join(os.getcwd(), src_dir)
+ import sphinx
+ sphinx.main(['runme',
+ '-D', 'version=%s' % metadata.get_version(),
+ os.path.join('docs',), os.path.join('build', 'docs')])
+
+
+addparams = {}
+if HAVE_PY2EXE:
+ addparams['console'] = [{'script': 'osc-wrapper.py', 'dest_base': 'osc', 'icon_resources': [(1, 'osc.ico')]}]
+ addparams['zipfile'] = 'shared.lib'
+ addparams['options'] = {'py2exe': {'optimize': 0, 'compressed': True, 'packages': ['xml.etree', 'StringIO', 'gzip']}}
+
+data_files = []
+if sys.platform[:3] != 'win':
+ data_files.append((os.path.join('share', 'man', 'man1'), [os.path.join('build', 'osc.1.gz')]))
+
+setup(name='osc',
+ version = osc.core.__version__,
+ description = 'openSUSE commander',
+ long_description = 'Command-line client for the openSUSE Build Service, which allows to access repositories in the openSUSE Build Service in similar way as Subversion repositories.',
+ author = 'openSUSE project',
+ author_email = 'opensuse-buildservice@opensuse.org',
+ license = 'GPL',
+ platforms = ['Linux', 'Mac OSX', 'Windows XP/2000/NT', 'Windows 95/98/ME', 'FreeBSD'],
+ keywords = ['openSUSE', 'SUSE', 'RPM', 'build', 'buildservice'],
+ url = 'http://en.opensuse.org/openSUSE:OSC',
+ download_url = 'https://github.com/openSUSE/osc',
+ packages = ['osc', 'osc.util'],
+ scripts = ['osc_hotshot.py', 'osc-wrapper.py'],
+ data_files = data_files,
+
+ # Override certain command classes with our own ones
+ cmdclass = {
+ 'build': build_osc,
+ 'build_docs' : build_docs,
+ },
+ **addparams
+ )
diff --git a/tests/addfile_fixtures/oscrc b/tests/addfile_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/addfile_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/addfile_fixtures/osctest/.osc/_apiurl b/tests/addfile_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/addfile_fixtures/osctest/.osc/_packages b/tests/addfile_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/addfile_fixtures/osctest/.osc/_project b/tests/addfile_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_apiurl b/tests/addfile_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_files b/tests/addfile_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_osclib_version b/tests/addfile_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_package b/tests/addfile_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_project b/tests/addfile_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/_to_be_deleted b/tests/addfile_fixtures/osctest/simple/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/foo b/tests/addfile_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/merge b/tests/addfile_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/addfile_fixtures/osctest/simple/.osc/nochange b/tests/addfile_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/addfile_fixtures/osctest/simple/merge b/tests/addfile_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/addfile_fixtures/osctest/simple/nochange b/tests/addfile_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/addfile_fixtures/osctest/simple/toadd1 b/tests/addfile_fixtures/osctest/simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/addfile_fixtures/osctest/simple/toadd2 b/tests/addfile_fixtures/osctest/simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/addfile_fixtures/osctest/simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/commit_fixtures/oscrc b/tests/commit_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/commit_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/commit_fixtures/osctest/.osc/_apiurl b/tests/commit_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/.osc/_packages b/tests/commit_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/commit_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/commit_fixtures/osctest/.osc/_project b/tests/commit_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/commit_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/commit_fixtures/osctest/add/.osc/_apiurl b/tests/commit_fixtures/osctest/add/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/add/.osc/_files b/tests/commit_fixtures/osctest/add/.osc/_files
new file mode 100644
index 0000000..6c3d53a
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="add" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/add/.osc/_meta b/tests/commit_fixtures/osctest/add/.osc/_meta
new file mode 100644
index 0000000..1b2b022
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="add" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/add/.osc/_osclib_version b/tests/commit_fixtures/osctest/add/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/add/.osc/_package b/tests/commit_fixtures/osctest/add/.osc/_package
new file mode 100644
index 0000000..76d4bb8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_package
@@ -0,0 +1 @@
+add
diff --git a/tests/commit_fixtures/osctest/add/.osc/_project b/tests/commit_fixtures/osctest/add/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/add/.osc/_to_be_added b/tests/commit_fixtures/osctest/add/.osc/_to_be_added
new file mode 100644
index 0000000..76d4bb8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/_to_be_added
@@ -0,0 +1 @@
+add
diff --git a/tests/commit_fixtures/osctest/add/.osc/foo b/tests/commit_fixtures/osctest/add/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/add/.osc/merge b/tests/commit_fixtures/osctest/add/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/add/.osc/nochange b/tests/commit_fixtures/osctest/add/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/add/add b/tests/commit_fixtures/osctest/add/add
new file mode 100644
index 0000000..b242c36
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/add
@@ -0,0 +1 @@
+added file
diff --git a/tests/commit_fixtures/osctest/add/exists b/tests/commit_fixtures/osctest/add/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/exists
diff --git a/tests/commit_fixtures/osctest/add/foo b/tests/commit_fixtures/osctest/add/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/add/merge b/tests/commit_fixtures/osctest/add/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/add/nochange b/tests/commit_fixtures/osctest/add/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/add/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_apiurl b/tests/commit_fixtures/osctest/added_missing/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_files b/tests/commit_fixtures/osctest/added_missing/.osc/_files
new file mode 100644
index 0000000..d474ef3
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_files
@@ -0,0 +1,3 @@
+<directory name="added_missing" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" skipped="True" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_meta b/tests/commit_fixtures/osctest/added_missing/.osc/_meta
new file mode 100644
index 0000000..b0a2f47
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="added_missing" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_osclib_version b/tests/commit_fixtures/osctest/added_missing/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_package b/tests/commit_fixtures/osctest/added_missing/.osc/_package
new file mode 100644
index 0000000..db0af96
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_package
@@ -0,0 +1 @@
+added_missing
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_project b/tests/commit_fixtures/osctest/added_missing/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/added_missing/.osc/_to_be_added b/tests/commit_fixtures/osctest/added_missing/.osc/_to_be_added
new file mode 100644
index 0000000..b03d55d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/.osc/_to_be_added
@@ -0,0 +1,2 @@
+add
+bar
diff --git a/tests/commit_fixtures/osctest/added_missing/bar b/tests/commit_fixtures/osctest/added_missing/bar
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/commit_fixtures/osctest/added_missing/bar
@@ -0,0 +1 @@
+foobar
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_apiurl b/tests/commit_fixtures/osctest/allstates/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_files b/tests/commit_fixtures/osctest/allstates/.osc/_files
new file mode 100644
index 0000000..02a576b
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_files
@@ -0,0 +1,8 @@
+<directory name="allstates" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="676513fde5797c3785164942c97dfec1" mtime="1283506309" name="missing" size="8" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="100" skipped="true" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_meta b/tests/commit_fixtures/osctest/allstates/.osc/_meta
new file mode 100644
index 0000000..510875e
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="allstates" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_osclib_version b/tests/commit_fixtures/osctest/allstates/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_package b/tests/commit_fixtures/osctest/allstates/.osc/_package
new file mode 100644
index 0000000..9d1ec82
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_package
@@ -0,0 +1 @@
+allstates
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_project b/tests/commit_fixtures/osctest/allstates/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_to_be_added b/tests/commit_fixtures/osctest/allstates/.osc/_to_be_added
new file mode 100644
index 0000000..cd0a2fe
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_to_be_added
@@ -0,0 +1,2 @@
+add
+missing
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/_to_be_deleted b/tests/commit_fixtures/osctest/allstates/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/foo b/tests/commit_fixtures/osctest/allstates/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/merge b/tests/commit_fixtures/osctest/allstates/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/missing b/tests/commit_fixtures/osctest/allstates/.osc/missing
new file mode 100644
index 0000000..33e45d5
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/missing
@@ -0,0 +1 @@
+missing
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/nochange b/tests/commit_fixtures/osctest/allstates/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/allstates/.osc/test b/tests/commit_fixtures/osctest/allstates/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/commit_fixtures/osctest/allstates/add b/tests/commit_fixtures/osctest/allstates/add
new file mode 100644
index 0000000..b242c36
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/add
@@ -0,0 +1 @@
+added file
diff --git a/tests/commit_fixtures/osctest/allstates/exists b/tests/commit_fixtures/osctest/allstates/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/exists
diff --git a/tests/commit_fixtures/osctest/allstates/missing b/tests/commit_fixtures/osctest/allstates/missing
new file mode 100644
index 0000000..feae347
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/missing
@@ -0,0 +1 @@
+replaced
diff --git a/tests/commit_fixtures/osctest/allstates/nochange b/tests/commit_fixtures/osctest/allstates/nochange
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/nochange
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/commit_fixtures/osctest/allstates/test b/tests/commit_fixtures/osctest/allstates/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/commit_fixtures/osctest/allstates/test
@@ -0,0 +1 @@
+test
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_apiurl b/tests/commit_fixtures/osctest/branch/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_files b/tests/commit_fixtures/osctest/branch/.osc/_files
new file mode 100644
index 0000000..8cbc407
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="unix" rev="9afa23b484de05e28364b18de7bb1432" srcmd5="9afa23b484de05e28364b18de7bb1432">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="cd21541fe2442d3d324a6d6103752913" package="unique" project="btest" srcmd5="b63634ab40861fdb8b44e5f4f459c621" />
+ <entry md5="75d884cf1d235180faec5acb63063972" mtime="1283525196" name="simple" size="21" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_meta b/tests/commit_fixtures/osctest/branch/.osc/_meta
new file mode 100644
index 0000000..6adc647
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_meta
@@ -0,0 +1,5 @@
+<package name="branch" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+ <person role="maintainer" userid="Admin"/>
+</package>
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_osclib_version b/tests/commit_fixtures/osctest/branch/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_package b/tests/commit_fixtures/osctest/branch/.osc/_package
new file mode 100644
index 0000000..80858c1
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_package
@@ -0,0 +1 @@
+branch
diff --git a/tests/commit_fixtures/osctest/branch/.osc/_project b/tests/commit_fixtures/osctest/branch/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/commit_fixtures/osctest/branch/.osc/simple b/tests/commit_fixtures/osctest/branch/.osc/simple
new file mode 100644
index 0000000..f425d9a
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/.osc/simple
@@ -0,0 +1 @@
+imple modified file.
diff --git a/tests/commit_fixtures/osctest/branch/cfilesremote b/tests/commit_fixtures/osctest/branch/cfilesremote
new file mode 100644
index 0000000..5fe6f29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/cfilesremote
@@ -0,0 +1,5 @@
+<directory name="branch" rev="5" srcmd5="1d4bbfa2655ab3982074226e16e1e5ff" vrev="5">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="1d4bbfa2655ab3982074226e16e1e5ff" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" xsrcmd5="87ea02aede261b0267aabaa97c756e7a" />
+ <entry md5="542f96b49b64095104d8a9e9dd313a9c" mtime="1283521153" name="_link" size="130" />
+ <entry md5="75da7f7167c22b2b02c6879366d78ad1" mtime="1283525027" name="simple" size="22" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/branch/files b/tests/commit_fixtures/osctest/branch/files
new file mode 100644
index 0000000..1f37d41
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/files
@@ -0,0 +1,4 @@
+<directory name="branch" rev="87ea02aede261b0267aabaa97c756e7a" srcmd5="87ea02aede261b0267aabaa97c756e7a">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="1d4bbfa2655ab3982074226e16e1e5ff" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" />
+ <entry md5="75da7f7167c22b2b02c6879366d78ad1" mtime="1283525027" name="simple" size="22" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/branch/filesremote b/tests/commit_fixtures/osctest/branch/filesremote
new file mode 100644
index 0000000..1bfc91b
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/filesremote
@@ -0,0 +1,5 @@
+<directory name="branch" rev="6" srcmd5="cd21541fe2442d3d324a6d6103752913" vrev="6">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="cd21541fe2442d3d324a6d6103752913" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" xsrcmd5="9afa23b484de05e28364b18de7bb1432" />
+ <entry md5="542f96b49b64095104d8a9e9dd313a9c" mtime="1283521153" name="_link" size="130" skipped="true" />
+ <entry md5="75d884cf1d235180faec5acb63063972" mtime="1283525196" name="simple" size="21" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/branch/simple b/tests/commit_fixtures/osctest/branch/simple
new file mode 100644
index 0000000..60627a5
--- /dev/null
+++ b/tests/commit_fixtures/osctest/branch/simple
@@ -0,0 +1 @@
+simple modified file.
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_apiurl b/tests/commit_fixtures/osctest/conflict/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_files b/tests/commit_fixtures/osctest/conflict/.osc/_files
new file mode 100644
index 0000000..a67ff42
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282130148" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282130148" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_in_conflict b/tests/commit_fixtures/osctest/conflict/.osc/_in_conflict
new file mode 100644
index 0000000..a00af07
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_in_conflict
@@ -0,0 +1 @@
+merge
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_meta b/tests/commit_fixtures/osctest/conflict/.osc/_meta
new file mode 100644
index 0000000..6f5b30d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="conflict" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_osclib_version b/tests/commit_fixtures/osctest/conflict/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_package b/tests/commit_fixtures/osctest/conflict/.osc/_package
new file mode 100644
index 0000000..783a0ef
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_package
@@ -0,0 +1 @@
+conflict \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/_project b/tests/commit_fixtures/osctest/conflict/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/foo b/tests/commit_fixtures/osctest/conflict/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/merge b/tests/commit_fixtures/osctest/conflict/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/conflict/.osc/nochange b/tests/commit_fixtures/osctest/conflict/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/conflict/foo b/tests/commit_fixtures/osctest/conflict/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/conflict/merge b/tests/commit_fixtures/osctest/conflict/merge
new file mode 100644
index 0000000..f4ff164
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/merge
@@ -0,0 +1,4 @@
+Is it possible
+to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/conflict/nochange b/tests/commit_fixtures/osctest/conflict/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/conflict/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_apiurl b/tests/commit_fixtures/osctest/delete/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_files b/tests/commit_fixtures/osctest/delete/.osc/_files
new file mode 100644
index 0000000..b8bf188
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="delete" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_meta b/tests/commit_fixtures/osctest/delete/.osc/_meta
new file mode 100644
index 0000000..3ff45d2
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="delete" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_osclib_version b/tests/commit_fixtures/osctest/delete/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_package b/tests/commit_fixtures/osctest/delete/.osc/_package
new file mode 100644
index 0000000..c8b1b42
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_package
@@ -0,0 +1 @@
+delete
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_project b/tests/commit_fixtures/osctest/delete/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/delete/.osc/_to_be_deleted b/tests/commit_fixtures/osctest/delete/.osc/_to_be_deleted
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/_to_be_deleted
@@ -0,0 +1 @@
+nochange
diff --git a/tests/commit_fixtures/osctest/delete/.osc/foo b/tests/commit_fixtures/osctest/delete/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/delete/.osc/merge b/tests/commit_fixtures/osctest/delete/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/delete/.osc/nochange b/tests/commit_fixtures/osctest/delete/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/delete/exists b/tests/commit_fixtures/osctest/delete/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/exists
diff --git a/tests/commit_fixtures/osctest/delete/foo b/tests/commit_fixtures/osctest/delete/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/delete/merge b/tests/commit_fixtures/osctest/delete/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/delete/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_apiurl b/tests/commit_fixtures/osctest/multiple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_files b/tests/commit_fixtures/osctest/multiple/.osc/_files
new file mode 100644
index 0000000..9df02ca
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="multiple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_meta b/tests/commit_fixtures/osctest/multiple/.osc/_meta
new file mode 100644
index 0000000..e8fea3d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="multiple" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_osclib_version b/tests/commit_fixtures/osctest/multiple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_package b/tests/commit_fixtures/osctest/multiple/.osc/_package
new file mode 100644
index 0000000..5c4139d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_package
@@ -0,0 +1 @@
+multiple
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_project b/tests/commit_fixtures/osctest/multiple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_to_be_added b/tests/commit_fixtures/osctest/multiple/.osc/_to_be_added
new file mode 100644
index 0000000..91e4e3d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_to_be_added
@@ -0,0 +1,2 @@
+add
+add2
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/_to_be_deleted b/tests/commit_fixtures/osctest/multiple/.osc/_to_be_deleted
new file mode 100644
index 0000000..cf978bc
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+foo
+merge
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/foo b/tests/commit_fixtures/osctest/multiple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/merge b/tests/commit_fixtures/osctest/multiple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/nochange b/tests/commit_fixtures/osctest/multiple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/multiple/.osc/test b/tests/commit_fixtures/osctest/multiple/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/commit_fixtures/osctest/multiple/add b/tests/commit_fixtures/osctest/multiple/add
new file mode 100644
index 0000000..b242c36
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/add
@@ -0,0 +1 @@
+added file
diff --git a/tests/commit_fixtures/osctest/multiple/add2 b/tests/commit_fixtures/osctest/multiple/add2
new file mode 100644
index 0000000..4755903
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/add2
@@ -0,0 +1 @@
+add2
diff --git a/tests/commit_fixtures/osctest/multiple/exists b/tests/commit_fixtures/osctest/multiple/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/exists
diff --git a/tests/commit_fixtures/osctest/multiple/nochange b/tests/commit_fixtures/osctest/multiple/nochange
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/nochange
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/commit_fixtures/osctest/multiple/test b/tests/commit_fixtures/osctest/multiple/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/commit_fixtures/osctest/multiple/test
@@ -0,0 +1 @@
+test
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_apiurl b/tests/commit_fixtures/osctest/nochanges/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_files b/tests/commit_fixtures/osctest/nochanges/.osc/_files
new file mode 100644
index 0000000..fee086c
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="nochanges" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" skipped="True" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_meta b/tests/commit_fixtures/osctest/nochanges/.osc/_meta
new file mode 100644
index 0000000..c2daf5c
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="nochanges" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_osclib_version b/tests/commit_fixtures/osctest/nochanges/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_package b/tests/commit_fixtures/osctest/nochanges/.osc/_package
new file mode 100644
index 0000000..40b16a9
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_package
@@ -0,0 +1 @@
+nochanges
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/_project b/tests/commit_fixtures/osctest/nochanges/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/merge b/tests/commit_fixtures/osctest/nochanges/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/nochanges/.osc/nochange b/tests/commit_fixtures/osctest/nochanges/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/nochanges/exists b/tests/commit_fixtures/osctest/nochanges/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/exists
diff --git a/tests/commit_fixtures/osctest/nochanges/nochange b/tests/commit_fixtures/osctest/nochanges/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/nochanges/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_apiurl b/tests/commit_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_files b/tests/commit_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_meta b/tests/commit_fixtures/osctest/simple/.osc/_meta
new file mode 100644
index 0000000..a40f92d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_meta
@@ -0,0 +1,4 @@
+<package name="simple" project="osctest">
+ <title>Title example</title>
+ <description>Description example</description>
+</package>
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_osclib_version b/tests/commit_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_package b/tests/commit_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/simple/.osc/_project b/tests/commit_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/commit_fixtures/osctest/simple/.osc/foo b/tests/commit_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/simple/.osc/merge b/tests/commit_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/simple/.osc/nochange b/tests/commit_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/commit_fixtures/osctest/simple/exists b/tests/commit_fixtures/osctest/simple/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/exists
diff --git a/tests/commit_fixtures/osctest/simple/foo b/tests/commit_fixtures/osctest/simple/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/commit_fixtures/osctest/simple/merge b/tests/commit_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/commit_fixtures/osctest/simple/nochange b/tests/commit_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/commit_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/commit_fixtures/testAddedMissing_cfilesremote b/tests/commit_fixtures/testAddedMissing_cfilesremote
new file mode 100644
index 0000000..a9c946e
--- /dev/null
+++ b/tests/commit_fixtures/testAddedMissing_cfilesremote
@@ -0,0 +1,4 @@
+<directory name="added_missing" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" mtime="1292622742" name="bar" size="7" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+</directory>
diff --git a/tests/commit_fixtures/testAddedMissing_filesremote b/tests/commit_fixtures/testAddedMissing_filesremote
new file mode 100644
index 0000000..03c7d89
--- /dev/null
+++ b/tests/commit_fixtures/testAddedMissing_filesremote
@@ -0,0 +1,3 @@
+<directory name="added_missing" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+</directory>
diff --git a/tests/commit_fixtures/testAddedMissing_lfilelist b/tests/commit_fixtures/testAddedMissing_lfilelist
new file mode 100644
index 0000000..c846f12
--- /dev/null
+++ b/tests/commit_fixtures/testAddedMissing_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="14758f1afd44c09b7992073ccf00b43d" name="bar" /><entry md5="0d62ceea6020d75154078a20d8c9f9ba" name="foo" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testAddedMissing_missingfilelist b/tests/commit_fixtures/testAddedMissing_missingfilelist
new file mode 100644
index 0000000..7070186
--- /dev/null
+++ b/tests/commit_fixtures/testAddedMissing_missingfilelist
@@ -0,0 +1,3 @@
+<directory error="missing" name="added_missing">
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" name="bar" />
+</directory>
diff --git a/tests/commit_fixtures/testAddfile_cfilesremote b/tests/commit_fixtures/testAddfile_cfilesremote
new file mode 100644
index 0000000..47538b0
--- /dev/null
+++ b/tests/commit_fixtures/testAddfile_cfilesremote
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" mtime="1111111111" name="add" size="11" />
+</directory>
diff --git a/tests/commit_fixtures/testAddfile_filesremote b/tests/commit_fixtures/testAddfile_filesremote
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/commit_fixtures/testAddfile_filesremote
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/testAddfile_lfilelist b/tests/commit_fixtures/testAddfile_lfilelist
new file mode 100644
index 0000000..c0c079b
--- /dev/null
+++ b/tests/commit_fixtures/testAddfile_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" /><entry md5="0d62ceea6020d75154078a20d8c9f9ba" name="foo" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /><entry md5="7efa70f68983fad1cf487f69dedf93e9" name="nochange" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testAddfile_missingfilelist b/tests/commit_fixtures/testAddfile_missingfilelist
new file mode 100644
index 0000000..ac41f20
--- /dev/null
+++ b/tests/commit_fixtures/testAddfile_missingfilelist
@@ -0,0 +1,3 @@
+<directory error="missing" name="add">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" />
+</directory>
diff --git a/tests/commit_fixtures/testAllStates_cfilesremote b/tests/commit_fixtures/testAllStates_cfilesremote
new file mode 100644
index 0000000..18e5fa2
--- /dev/null
+++ b/tests/commit_fixtures/testAllStates_cfilesremote
@@ -0,0 +1,8 @@
+<directory name="allstates" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" mtime="3333333333" name="add" size="11" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="d908d26cac8092d475f40a5179ca6347" mtime="4444444444" name="missing" size="9" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="2222222222" name="nochange" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="100" />
+</directory>
diff --git a/tests/commit_fixtures/testAllStates_expfiles b/tests/commit_fixtures/testAllStates_expfiles
new file mode 100644
index 0000000..692f070
--- /dev/null
+++ b/tests/commit_fixtures/testAllStates_expfiles
@@ -0,0 +1,8 @@
+<directory name="allstates" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" mtime="3333333333" name="add" size="11" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="d908d26cac8092d475f40a5179ca6347" mtime="4444444444" name="missing" size="9" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="2222222222" name="nochange" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="100" skipped="true" />
+</directory>
diff --git a/tests/commit_fixtures/testAllStates_filesremote b/tests/commit_fixtures/testAllStates_filesremote
new file mode 100644
index 0000000..995a585
--- /dev/null
+++ b/tests/commit_fixtures/testAllStates_filesremote
@@ -0,0 +1,8 @@
+<directory name="allstates" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="676513fde5797c3785164942c97dfec1" mtime="1283506309" name="missing" size="8" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="100" />
+</directory>
diff --git a/tests/commit_fixtures/testAllStates_lfilelist b/tests/commit_fixtures/testAllStates_lfilelist
new file mode 100644
index 0000000..c534c8c
--- /dev/null
+++ b/tests/commit_fixtures/testAllStates_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /><entry md5="d908d26cac8092d475f40a5179ca6347" name="missing" /><entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" /><entry md5="ffffffffffffffffffffffffffffffff" name="skipped" /><entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" name="test" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testAllStates_missingfilelist b/tests/commit_fixtures/testAllStates_missingfilelist
new file mode 100644
index 0000000..d7ee788
--- /dev/null
+++ b/tests/commit_fixtures/testAllStates_missingfilelist
@@ -0,0 +1,5 @@
+<directory error="missing" name="allstates">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" />
+ <entry md5="d908d26cac8092d475f40a5179ca6347" name="missing" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" />
+</directory>
diff --git a/tests/commit_fixtures/testConflictfile_filesremote b/tests/commit_fixtures/testConflictfile_filesremote
new file mode 100644
index 0000000..a67ff42
--- /dev/null
+++ b/tests/commit_fixtures/testConflictfile_filesremote
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282130148" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282130148" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testDeletefile_cfilesremote b/tests/commit_fixtures/testDeletefile_cfilesremote
new file mode 100644
index 0000000..20ce708
--- /dev/null
+++ b/tests/commit_fixtures/testDeletefile_cfilesremote
@@ -0,0 +1,4 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+</directory>
diff --git a/tests/commit_fixtures/testDeletefile_filesremote b/tests/commit_fixtures/testDeletefile_filesremote
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/commit_fixtures/testDeletefile_filesremote
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/testDeletefile_lfilelist b/tests/commit_fixtures/testDeletefile_lfilelist
new file mode 100644
index 0000000..bc0f1c6
--- /dev/null
+++ b/tests/commit_fixtures/testDeletefile_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="0d62ceea6020d75154078a20d8c9f9ba" name="foo" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testExpand_cfilesremote b/tests/commit_fixtures/testExpand_cfilesremote
new file mode 100644
index 0000000..475cff8
--- /dev/null
+++ b/tests/commit_fixtures/testExpand_cfilesremote
@@ -0,0 +1,5 @@
+<directory name="branch" rev="7" srcmd5="1d4bbfa2655ab3982074226e16e1e5ff" vrev="7">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="1d4bbfa2655ab3982074226e16e1e5ff" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" xsrcmd5="87ea02aede261b0267aabaa97c756e7a" />
+ <entry md5="542f96b49b64095104d8a9e9dd313a9c" mtime="1283521153" name="_link" size="130" />
+ <entry md5="75da7f7167c22b2b02c6879366d78ad1" mtime="1283525027" name="simple" size="22" />
+</directory>
diff --git a/tests/commit_fixtures/testExpand_expandedfilesremote b/tests/commit_fixtures/testExpand_expandedfilesremote
new file mode 100644
index 0000000..1f37d41
--- /dev/null
+++ b/tests/commit_fixtures/testExpand_expandedfilesremote
@@ -0,0 +1,4 @@
+<directory name="branch" rev="87ea02aede261b0267aabaa97c756e7a" srcmd5="87ea02aede261b0267aabaa97c756e7a">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="1d4bbfa2655ab3982074226e16e1e5ff" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" />
+ <entry md5="75da7f7167c22b2b02c6879366d78ad1" mtime="1283525027" name="simple" size="22" />
+</directory>
diff --git a/tests/commit_fixtures/testExpand_filesremote b/tests/commit_fixtures/testExpand_filesremote
new file mode 100644
index 0000000..1bfc91b
--- /dev/null
+++ b/tests/commit_fixtures/testExpand_filesremote
@@ -0,0 +1,5 @@
+<directory name="branch" rev="6" srcmd5="cd21541fe2442d3d324a6d6103752913" vrev="6">
+ <linkinfo baserev="b63634ab40861fdb8b44e5f4f459c621" lsrcmd5="cd21541fe2442d3d324a6d6103752913" package="bar" project="foo" srcmd5="b63634ab40861fdb8b44e5f4f459c621" xsrcmd5="9afa23b484de05e28364b18de7bb1432" />
+ <entry md5="542f96b49b64095104d8a9e9dd313a9c" mtime="1283521153" name="_link" size="130" skipped="true" />
+ <entry md5="75d884cf1d235180faec5acb63063972" mtime="1283525196" name="simple" size="21" />
+</directory>
diff --git a/tests/commit_fixtures/testExpand_lfilelist b/tests/commit_fixtures/testExpand_lfilelist
new file mode 100644
index 0000000..d6d37b5
--- /dev/null
+++ b/tests/commit_fixtures/testExpand_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="75da7f7167c22b2b02c6879366d78ad1" name="simple" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testExpand_missingfilelist b/tests/commit_fixtures/testExpand_missingfilelist
new file mode 100644
index 0000000..f2d91e8
--- /dev/null
+++ b/tests/commit_fixtures/testExpand_missingfilelist
@@ -0,0 +1,3 @@
+<directory error="missing" name="branch">
+ <entry md5="75da7f7167c22b2b02c6879366d78ad1" name="simple" />
+</directory>
diff --git a/tests/commit_fixtures/testInterrupted_lfilelist b/tests/commit_fixtures/testInterrupted_lfilelist
new file mode 100644
index 0000000..85e9db5
--- /dev/null
+++ b/tests/commit_fixtures/testInterrupted_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="0d62ceea6020d75154078a20d8c9f9ba" name="foo" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /><entry md5="382588b92f5976de693f44c4d6df27b7" name="nochange" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testMultiple_cfilesremote b/tests/commit_fixtures/testMultiple_cfilesremote
new file mode 100644
index 0000000..e98188d
--- /dev/null
+++ b/tests/commit_fixtures/testMultiple_cfilesremote
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" mtime="1111111111" name="add" size="11" />
+ <entry md5="ea467af882b32a275fe62eb05aba6ee1" mtime="0000000000" name="add2" size="5" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="2222222222" name="nochange" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/commit_fixtures/testMultiple_filesremote b/tests/commit_fixtures/testMultiple_filesremote
new file mode 100644
index 0000000..00e4458
--- /dev/null
+++ b/tests/commit_fixtures/testMultiple_filesremote
@@ -0,0 +1,6 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/commit_fixtures/testMultiple_lfilelist b/tests/commit_fixtures/testMultiple_lfilelist
new file mode 100644
index 0000000..cd8c43c
--- /dev/null
+++ b/tests/commit_fixtures/testMultiple_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" /><entry md5="ea467af882b32a275fe62eb05aba6ee1" name="add2" /><entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" /><entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" name="test" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testMultiple_missingfilelist b/tests/commit_fixtures/testMultiple_missingfilelist
new file mode 100644
index 0000000..e4f6314
--- /dev/null
+++ b/tests/commit_fixtures/testMultiple_missingfilelist
@@ -0,0 +1,5 @@
+<directory error="missing" name="add">
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" />
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" />
+ <entry md5="ea467af882b32a275fe62eb05aba6ee1" name="add2" />
+</directory>
diff --git a/tests/commit_fixtures/testNoChanges_filesremote b/tests/commit_fixtures/testNoChanges_filesremote
new file mode 100644
index 0000000..a67ff42
--- /dev/null
+++ b/tests/commit_fixtures/testNoChanges_filesremote
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282130148" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282130148" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testPartial_cfilesremote b/tests/commit_fixtures/testPartial_cfilesremote
new file mode 100644
index 0000000..db75949
--- /dev/null
+++ b/tests/commit_fixtures/testPartial_cfilesremote
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" mtime="1111111111" name="add" size="11" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="2222222222" name="nochange" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/commit_fixtures/testPartial_filesremote b/tests/commit_fixtures/testPartial_filesremote
new file mode 100644
index 0000000..00e4458
--- /dev/null
+++ b/tests/commit_fixtures/testPartial_filesremote
@@ -0,0 +1,6 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/commit_fixtures/testPartial_lfilelist b/tests/commit_fixtures/testPartial_lfilelist
new file mode 100644
index 0000000..6da9126
--- /dev/null
+++ b/tests/commit_fixtures/testPartial_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /><entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" /><entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" name="test" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testPartial_missingfilelist b/tests/commit_fixtures/testPartial_missingfilelist
new file mode 100644
index 0000000..589642c
--- /dev/null
+++ b/tests/commit_fixtures/testPartial_missingfilelist
@@ -0,0 +1,4 @@
+<directory error="missing" name="partial">
+ <entry md5="b423d194c75e59ee4d8d2e07ba24323d" name="add" />
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" name="nochange" />
+</directory>
diff --git a/tests/commit_fixtures/testSimple_cfilesremote b/tests/commit_fixtures/testSimple_cfilesremote
new file mode 100644
index 0000000..bc155b4
--- /dev/null
+++ b/tests/commit_fixtures/testSimple_cfilesremote
@@ -0,0 +1,5 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="382588b92f5976de693f44c4d6df27b7" mtime="1282047303" name="nochange" size="41" />
+</directory>
diff --git a/tests/commit_fixtures/testSimple_filesremote b/tests/commit_fixtures/testSimple_filesremote
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/commit_fixtures/testSimple_filesremote
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/commit_fixtures/testSimple_lfilelist b/tests/commit_fixtures/testSimple_lfilelist
new file mode 100644
index 0000000..85e9db5
--- /dev/null
+++ b/tests/commit_fixtures/testSimple_lfilelist
@@ -0,0 +1 @@
+<directory><entry md5="0d62ceea6020d75154078a20d8c9f9ba" name="foo" /><entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" name="merge" /><entry md5="382588b92f5976de693f44c4d6df27b7" name="nochange" /></directory> \ No newline at end of file
diff --git a/tests/commit_fixtures/testSimple_missingfilelist b/tests/commit_fixtures/testSimple_missingfilelist
new file mode 100644
index 0000000..1c6bc68
--- /dev/null
+++ b/tests/commit_fixtures/testSimple_missingfilelist
@@ -0,0 +1,3 @@
+<directory error="missing" name="simple">
+ <entry md5="c4eaea5dcaff13418e38e7fea151dd49" name="nochange" />
+</directory>
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 0000000..0465792
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,207 @@
+import unittest
+import osc.core
+import shutil
+import tempfile
+import os
+import sys
+from xml.etree import cElementTree as ET
+EXPECTED_REQUESTS = []
+
+if sys.version_info[0:2] in ((2, 6), (2, 7)):
+ bytes = lambda x, *args: x
+
+try:
+ #python 2.x
+ from cStringIO import StringIO
+ from urllib2 import HTTPHandler, addinfourl, build_opener
+ from urlparse import urlparse, parse_qs
+except ImportError:
+ from io import StringIO
+ from urllib.request import HTTPHandler, addinfourl, build_opener
+ from urllib.parse import urlparse, parse_qs
+
+def urlcompare(url, *args):
+ """compare all components of url except query string - it is converted to
+ dict, therefor different ordering does not makes url's different, as well
+ as quoting of a query string"""
+
+ components = urlparse(url)
+ query_args = parse_qs(components.query)
+ components = components._replace(query=None)
+
+ if not args:
+ return False
+
+ for url in args:
+ components2 = urlparse(url)
+ query_args2 = parse_qs(components2.query)
+ components2 = components2._replace(query=None)
+
+ if components != components2 or \
+ query_args != query_args2:
+ return False
+
+ return True
+
+class RequestWrongOrder(Exception):
+ """raised if an unexpected request is issued to urllib2"""
+ def __init__(self, url, exp_url, method, exp_method):
+ Exception.__init__(self)
+ self.url = url
+ self.exp_url = exp_url
+ self.method = method
+ self.exp_method = exp_method
+
+ def __str__(self):
+ return '%s, %s, %s, %s' % (self.url, self.exp_url, self.method, self.exp_method)
+
+class RequestDataMismatch(Exception):
+ """raised if POSTed or PUTed data doesn't match with the expected data"""
+ def __init__(self, url, got, exp):
+ self.url = url
+ self.got = got
+ self.exp = exp
+
+ def __str__(self):
+ return '%s, %s, %s' % (self.url, self.got, self.exp)
+
+class MyHTTPHandler(HTTPHandler):
+ def __init__(self, exp_requests, fixtures_dir):
+ HTTPHandler.__init__(self)
+ self.__exp_requests = exp_requests
+ self.__fixtures_dir = fixtures_dir
+
+ def http_open(self, req):
+ r = self.__exp_requests.pop(0)
+ if not urlcompare(req.get_full_url(), r[1]) or req.get_method() != r[0]:
+ raise RequestWrongOrder(req.get_full_url(), r[1], req.get_method(), r[0])
+ if req.get_method() in ('GET', 'DELETE'):
+ return self.__mock_GET(r[1], **r[2])
+ elif req.get_method() in ('PUT', 'POST'):
+ return self.__mock_PUT(req, **r[2])
+
+ def __mock_GET(self, fullurl, **kwargs):
+ return self.__get_response(fullurl, **kwargs)
+
+ def __mock_PUT(self, req, **kwargs):
+ exp = kwargs.get('exp', None)
+ if exp is not None and 'expfile' in kwargs:
+ raise RuntimeError('either specify exp or expfile')
+ elif 'expfile' in kwargs:
+ exp = open(os.path.join(self.__fixtures_dir, kwargs['expfile']), 'r').read()
+ elif exp is None:
+ raise RuntimeError('exp or expfile required')
+ if exp is not None:
+ # use req.data instead of req.get_data() for python3 compatiblity
+ if req.data != bytes(exp, "utf-8"):
+ raise RequestDataMismatch(req.get_full_url(), repr(req.get_data()), repr(exp))
+ return self.__get_response(req.get_full_url(), **kwargs)
+
+ def __get_response(self, url, **kwargs):
+ f = None
+ if 'exception' in kwargs:
+ raise kwargs['exception']
+ if 'text' not in kwargs and 'file' in kwargs:
+ f = StringIO(open(os.path.join(self.__fixtures_dir, kwargs['file']), 'r').read())
+ elif 'text' in kwargs and 'file' not in kwargs:
+ f = StringIO(kwargs['text'])
+ else:
+ raise RuntimeError('either specify text or file')
+ resp = addinfourl(f, {}, url)
+ resp.code = kwargs.get('code', 200)
+ resp.msg = ''
+ return resp
+
+def urldecorator(method, fullurl, **kwargs):
+ def decorate(test_method):
+ def wrapped_test_method(*args):
+ addExpectedRequest(method, fullurl, **kwargs)
+ test_method(*args)
+ # "rename" method otherwise we cannot specify a TestCaseClass.testName
+ # cmdline arg when using unittest.main()
+ wrapped_test_method.__name__ = test_method.__name__
+ return wrapped_test_method
+ return decorate
+
+def GET(fullurl, **kwargs):
+ return urldecorator('GET', fullurl, **kwargs)
+
+def PUT(fullurl, **kwargs):
+ return urldecorator('PUT', fullurl, **kwargs)
+
+def POST(fullurl, **kwargs):
+ return urldecorator('POST', fullurl, **kwargs)
+
+def DELETE(fullurl, **kwargs):
+ return urldecorator('DELETE', fullurl, **kwargs)
+
+def addExpectedRequest(method, url, **kwargs):
+ global EXPECTED_REQUESTS
+ EXPECTED_REQUESTS.append((method, url, kwargs))
+
+class OscTestCase(unittest.TestCase):
+ def setUp(self, copytree=True):
+ oscrc = os.path.join(self._get_fixtures_dir(), 'oscrc')
+ osc.core.conf.get_config(override_conffile=oscrc,
+ override_no_keyring=True, override_no_gnome_keyring=True)
+ os.environ['OSC_CONFIG'] = oscrc
+
+ self.tmpdir = tempfile.mkdtemp(prefix='osc_test')
+ if copytree:
+ shutil.copytree(os.path.join(self._get_fixtures_dir(), 'osctest'), os.path.join(self.tmpdir, 'osctest'))
+ global EXPECTED_REQUESTS
+ EXPECTED_REQUESTS = []
+ osc.core.conf._build_opener = lambda u: build_opener(MyHTTPHandler(EXPECTED_REQUESTS, self._get_fixtures_dir()))
+ self.stdout = sys.stdout
+ sys.stdout = StringIO()
+
+ def tearDown(self):
+ self.assertTrue(len(EXPECTED_REQUESTS) == 0)
+ sys.stdout = self.stdout
+ try:
+ shutil.rmtree(self.tmpdir)
+ except:
+ pass
+
+ def _get_fixtures_dir(self):
+ raise NotImplementedError('subclasses should implement this method')
+
+ def _change_to_pkg(self, name):
+ os.chdir(os.path.join(self.tmpdir, 'osctest', name))
+
+ def _check_list(self, fname, exp):
+ fname = os.path.join('.osc', fname)
+ self.assertTrue(os.path.exists(fname))
+ self.assertEqual(open(fname, 'r').read(), exp)
+
+ def _check_addlist(self, exp):
+ self._check_list('_to_be_added', exp)
+
+ def _check_deletelist(self, exp):
+ self._check_list('_to_be_deleted', exp)
+
+ def _check_conflictlist(self, exp):
+ self._check_list('_in_conflict', exp)
+
+ def _check_status(self, p, fname, exp):
+ self.assertEqual(p.status(fname), exp)
+
+ def _check_digests(self, fname, *skipfiles):
+ fname = os.path.join(self._get_fixtures_dir(), fname)
+ self.assertEqual(open(os.path.join('.osc', '_files'), 'r').read(), open(fname, 'r').read())
+ root = ET.parse(fname).getroot()
+ for i in root.findall('entry'):
+ if i.get('name') in skipfiles:
+ continue
+ self.assertTrue(os.path.exists(os.path.join('.osc', i.get('name'))))
+ self.assertEqual(osc.core.dgst(os.path.join('.osc', i.get('name'))), i.get('md5'))
+
+ def assertEqualMultiline(self, got, exp):
+ if (got + exp).find('\n') == -1:
+ self.assertEqual(got, exp)
+ else:
+ start_delim = "\n" + (" 8< ".join(["-----"] * 8)) + "\n"
+ end_delim = "\n" + (" >8 ".join(["-----"] * 8)) + "\n\n"
+ self.assertEqual(got, exp,
+ "got:" + start_delim + got + end_delim +
+ "expected:" + start_delim + exp + end_delim)
diff --git a/tests/conf_fixtures/oscrc b/tests/conf_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/conf_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/deletefile_fixtures/oscrc b/tests/deletefile_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/deletefile_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/deletefile_fixtures/osctest/.osc/_apiurl b/tests/deletefile_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/.osc/_packages b/tests/deletefile_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/deletefile_fixtures/osctest/.osc/_project b/tests/deletefile_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_apiurl b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_files b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_osclib_version b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_package b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_project b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_added b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_deleted b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/foo b/tests/deletefile_fixtures/osctest/already_deleted/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/merge b/tests/deletefile_fixtures/osctest/already_deleted/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/.osc/nochange b/tests/deletefile_fixtures/osctest/already_deleted/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/merge b/tests/deletefile_fixtures/osctest/already_deleted/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/nochange b/tests/deletefile_fixtures/osctest/already_deleted/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/toadd1 b/tests/deletefile_fixtures/osctest/already_deleted/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/already_deleted/toadd2 b/tests/deletefile_fixtures/osctest/already_deleted/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/already_deleted/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_apiurl b/tests/deletefile_fixtures/osctest/conflict/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_files b/tests/deletefile_fixtures/osctest/conflict/.osc/_files
new file mode 100644
index 0000000..6fc0c34
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_in_conflict b/tests/deletefile_fixtures/osctest/conflict/.osc/_in_conflict
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_in_conflict
@@ -0,0 +1 @@
+foo
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_osclib_version b/tests/deletefile_fixtures/osctest/conflict/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_package b/tests/deletefile_fixtures/osctest/conflict/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_project b/tests/deletefile_fixtures/osctest/conflict/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/_to_be_added b/tests/deletefile_fixtures/osctest/conflict/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/foo b/tests/deletefile_fixtures/osctest/conflict/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/merge b/tests/deletefile_fixtures/osctest/conflict/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/conflict/.osc/nochange b/tests/deletefile_fixtures/osctest/conflict/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/deletefile_fixtures/osctest/conflict/foo b/tests/deletefile_fixtures/osctest/conflict/foo
new file mode 100644
index 0000000..ad9621d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/foo
@@ -0,0 +1,5 @@
+<<<<<<< foo.mine
+This is no test.
+=======
+This is a simple test.
+>>>>>>> foo.r2
diff --git a/tests/deletefile_fixtures/osctest/conflict/foo.mine b/tests/deletefile_fixtures/osctest/conflict/foo.mine
new file mode 100644
index 0000000..3543613
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/foo.mine
@@ -0,0 +1 @@
+This is no test.
diff --git a/tests/deletefile_fixtures/osctest/conflict/foo.r2 b/tests/deletefile_fixtures/osctest/conflict/foo.r2
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/foo.r2
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/conflict/merge b/tests/deletefile_fixtures/osctest/conflict/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/conflict/nochange b/tests/deletefile_fixtures/osctest/conflict/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/deletefile_fixtures/osctest/conflict/toadd1 b/tests/deletefile_fixtures/osctest/conflict/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/conflict/toadd2 b/tests/deletefile_fixtures/osctest/conflict/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/conflict/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_apiurl b/tests/deletefile_fixtures/osctest/delete/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_files b/tests/deletefile_fixtures/osctest/delete/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_osclib_version b/tests/deletefile_fixtures/osctest/delete/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_package b/tests/deletefile_fixtures/osctest/delete/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_project b/tests/deletefile_fixtures/osctest/delete/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_added b/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_deleted b/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/foo b/tests/deletefile_fixtures/osctest/delete/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/merge b/tests/deletefile_fixtures/osctest/delete/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/delete/.osc/nochange b/tests/deletefile_fixtures/osctest/delete/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/deletefile_fixtures/osctest/delete/merge b/tests/deletefile_fixtures/osctest/delete/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/delete/nochange b/tests/deletefile_fixtures/osctest/delete/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/deletefile_fixtures/osctest/delete/toadd2 b/tests/deletefile_fixtures/osctest/delete/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/delete/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_apiurl b/tests/deletefile_fixtures/osctest/replace/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_files b/tests/deletefile_fixtures/osctest/replace/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_osclib_version b/tests/deletefile_fixtures/osctest/replace/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_package b/tests/deletefile_fixtures/osctest/replace/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_project b/tests/deletefile_fixtures/osctest/replace/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/_to_be_added b/tests/deletefile_fixtures/osctest/replace/.osc/_to_be_added
new file mode 100644
index 0000000..d530a9a
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/_to_be_added
@@ -0,0 +1,2 @@
+toadd1
+merge
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/foo b/tests/deletefile_fixtures/osctest/replace/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/merge b/tests/deletefile_fixtures/osctest/replace/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/replace/.osc/nochange b/tests/deletefile_fixtures/osctest/replace/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/deletefile_fixtures/osctest/replace/foo b/tests/deletefile_fixtures/osctest/replace/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/replace/merge b/tests/deletefile_fixtures/osctest/replace/merge
new file mode 100644
index 0000000..feae347
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/merge
@@ -0,0 +1 @@
+replaced
diff --git a/tests/deletefile_fixtures/osctest/replace/nochange b/tests/deletefile_fixtures/osctest/replace/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/deletefile_fixtures/osctest/replace/toadd1 b/tests/deletefile_fixtures/osctest/replace/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/replace/toadd2 b/tests/deletefile_fixtures/osctest/replace/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/replace/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_apiurl b/tests/deletefile_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_files b/tests/deletefile_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..eb2a44c
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,7 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" mtime="123456789" name="skipped" size="225" skipped="true" />
+ <entry md5="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" mtime="012345678" name="skipped_exists" size="22" skipped="true" />
+</directory>
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_osclib_version b/tests/deletefile_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_package b/tests/deletefile_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_project b/tests/deletefile_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/_to_be_added b/tests/deletefile_fixtures/osctest/simple/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/foo b/tests/deletefile_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/merge b/tests/deletefile_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/simple/.osc/nochange b/tests/deletefile_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/deletefile_fixtures/osctest/simple/foo b/tests/deletefile_fixtures/osctest/simple/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/deletefile_fixtures/osctest/simple/merge b/tests/deletefile_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/deletefile_fixtures/osctest/simple/nochange b/tests/deletefile_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/deletefile_fixtures/osctest/simple/skipped_exists b/tests/deletefile_fixtures/osctest/simple/skipped_exists
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/skipped_exists
@@ -0,0 +1 @@
+foobar
diff --git a/tests/deletefile_fixtures/osctest/simple/toadd1 b/tests/deletefile_fixtures/osctest/simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/deletefile_fixtures/osctest/simple/toadd2 b/tests/deletefile_fixtures/osctest/simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/deletefile_fixtures/osctest/simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/oscrc b/tests/difffile_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/difffile_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/difffile_fixtures/osctest/.osc/_apiurl b/tests/difffile_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/.osc/_packages b/tests/difffile_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/difffile_fixtures/osctest/.osc/_project b/tests/difffile_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_apiurl b/tests/difffile_fixtures/osctest/binary/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_files b/tests/difffile_fixtures/osctest/binary/.osc/_files
new file mode 100644
index 0000000..2c82894
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="binary" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="8f618462e00017108b4146a29e074bdf" mtime="1111111111" name="binary" size="18" />
+ <entry md5="ee813c93cb5730dce38976695634482f" mtime="1111111111" name="binary_deleted" size="26" />
+</directory>
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_osclib_version b/tests/difffile_fixtures/osctest/binary/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_package b/tests/difffile_fixtures/osctest/binary/.osc/_package
new file mode 100644
index 0000000..a9128c2
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_package
@@ -0,0 +1 @@
+binary
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_project b/tests/difffile_fixtures/osctest/binary/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_to_be_added b/tests/difffile_fixtures/osctest/binary/.osc/_to_be_added
new file mode 100644
index 0000000..075a151
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_to_be_added
@@ -0,0 +1 @@
+binary_added
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/_to_be_deleted b/tests/difffile_fixtures/osctest/binary/.osc/_to_be_deleted
new file mode 100644
index 0000000..705639e
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/_to_be_deleted
@@ -0,0 +1 @@
+binary_deleted
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/binary b/tests/difffile_fixtures/osctest/binary/.osc/binary
new file mode 100644
index 0000000..727c366
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/binary
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/binary/.osc/binary_deleted b/tests/difffile_fixtures/osctest/binary/.osc/binary_deleted
new file mode 100644
index 0000000..17e35ec
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/.osc/binary_deleted
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/binary/binary b/tests/difffile_fixtures/osctest/binary/binary
new file mode 100644
index 0000000..5868978
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/binary
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/binary/binary_added b/tests/difffile_fixtures/osctest/binary/binary_added
new file mode 100644
index 0000000..188a937
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/binary/binary_added
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_apiurl b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_files b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_osclib_version b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_package b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_package
new file mode 100644
index 0000000..5d09f91
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_package
@@ -0,0 +1 @@
+remote_localdelete
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_project b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_to_be_deleted b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_to_be_deleted
new file mode 100644
index 0000000..a00af07
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/_to_be_deleted
@@ -0,0 +1 @@
+merge
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/foo b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/merge b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/.osc/nochange b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/foo b/tests/difffile_fixtures/osctest/remote_localdelete/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/nochange b/tests/difffile_fixtures/osctest/remote_localdelete/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_localdelete/toadd2 b/tests/difffile_fixtures/osctest/remote_localdelete/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localdelete/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_apiurl b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_files b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_files
new file mode 100644
index 0000000..7c4480a
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="b1b642cdbacf9956104f8565e297ed00" mtime="1283246089" name="binary" size="27" />
+</directory>
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_osclib_version b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_package b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_package
new file mode 100644
index 0000000..6c6c3a8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_package
@@ -0,0 +1 @@
+remote_localmodified
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_project b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/binary b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/binary
new file mode 100644
index 0000000..5868978
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/binary
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/foo b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/merge b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/.osc/nochange b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/binary b/tests/difffile_fixtures/osctest/remote_localmodified/binary
new file mode 100644
index 0000000..ff2abf9
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/binary
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/foo b/tests/difffile_fixtures/osctest/remote_localmodified/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/merge b/tests/difffile_fixtures/osctest/remote_localmodified/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/nochange b/tests/difffile_fixtures/osctest/remote_localmodified/nochange
new file mode 100644
index 0000000..a64acb7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/nochange
@@ -0,0 +1,2 @@
+This file didn't change.
+oh it does
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/toadd1 b/tests/difffile_fixtures/osctest/remote_localmodified/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/difffile_fixtures/osctest/remote_localmodified/toadd2 b/tests/difffile_fixtures/osctest/remote_localmodified/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_localmodified/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_apiurl b/tests/difffile_fixtures/osctest/remote_simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_files b/tests/difffile_fixtures/osctest/remote_simple/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_osclib_version b/tests/difffile_fixtures/osctest/remote_simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_package b/tests/difffile_fixtures/osctest/remote_simple/.osc/_package
new file mode 100644
index 0000000..7894c46
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_package
@@ -0,0 +1 @@
+remote_simple
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_project b/tests/difffile_fixtures/osctest/remote_simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/_to_be_added b/tests/difffile_fixtures/osctest/remote_simple/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/foo b/tests/difffile_fixtures/osctest/remote_simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/merge b/tests/difffile_fixtures/osctest/remote_simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_simple/.osc/nochange b/tests/difffile_fixtures/osctest/remote_simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_simple/binary b/tests/difffile_fixtures/osctest/remote_simple/binary
new file mode 100644
index 0000000..5868978
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/binary
Binary files differ
diff --git a/tests/difffile_fixtures/osctest/remote_simple/foo b/tests/difffile_fixtures/osctest/remote_simple/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_simple/merge b/tests/difffile_fixtures/osctest/remote_simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_simple/nochange b/tests/difffile_fixtures/osctest/remote_simple/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_simple/toadd1 b/tests/difffile_fixtures/osctest/remote_simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/difffile_fixtures/osctest/remote_simple/toadd2 b/tests/difffile_fixtures/osctest/remote_simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_apiurl b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_files b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_osclib_version b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_package b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_package
new file mode 100644
index 0000000..6a70072
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_package
@@ -0,0 +1 @@
+remote_simple_noadd
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_project b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/foo b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/merge b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/nochange b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/foo b/tests/difffile_fixtures/osctest/remote_simple_noadd/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/merge b/tests/difffile_fixtures/osctest/remote_simple_noadd/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/nochange b/tests/difffile_fixtures/osctest/remote_simple_noadd/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/remote_simple_noadd/toadd2 b/tests/difffile_fixtures/osctest/remote_simple_noadd/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/remote_simple_noadd/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_apiurl b/tests/difffile_fixtures/osctest/replaced/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_files b/tests/difffile_fixtures/osctest/replaced/.osc/_files
new file mode 100644
index 0000000..c275f82
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_files
@@ -0,0 +1,3 @@
+<directory name="replaced" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="81be947db54c2e225dc8eacce64d8a4a" mtime="1282731457" name="replaced" size="17" />
+</directory>
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_osclib_version b/tests/difffile_fixtures/osctest/replaced/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_package b/tests/difffile_fixtures/osctest/replaced/.osc/_package
new file mode 100644
index 0000000..feae347
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_package
@@ -0,0 +1 @@
+replaced
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_project b/tests/difffile_fixtures/osctest/replaced/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/_to_be_added b/tests/difffile_fixtures/osctest/replaced/.osc/_to_be_added
new file mode 100644
index 0000000..feae347
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/_to_be_added
@@ -0,0 +1 @@
+replaced
diff --git a/tests/difffile_fixtures/osctest/replaced/.osc/replaced b/tests/difffile_fixtures/osctest/replaced/.osc/replaced
new file mode 100644
index 0000000..7c3f1a8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/.osc/replaced
@@ -0,0 +1 @@
+yet another file
diff --git a/tests/difffile_fixtures/osctest/replaced/replaced b/tests/difffile_fixtures/osctest/replaced/replaced
new file mode 100644
index 0000000..f479fb8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/replaced/replaced
@@ -0,0 +1 @@
+foo replaced
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_apiurl b/tests/difffile_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_files b/tests/difffile_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..041f606
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,9 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="eb9c2bf0eb63f3a7bc0ea37ef18aeba5" mtime="1282730880" name="somefile" size="13" />
+ <entry md5="81be947db54c2e225dc8eacce64d8a4a" mtime="1282731457" name="replaced" size="17" />
+ <entry md5="676513fde5797c3785164942c97dfec1" mtime="1282731738" name="missing" size="8" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="12" skipped="true" />
+</directory>
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_in_conflict b/tests/difffile_fixtures/osctest/simple/.osc/_in_conflict
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_in_conflict
@@ -0,0 +1 @@
+foo
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_osclib_version b/tests/difffile_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_package b/tests/difffile_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_project b/tests/difffile_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_to_be_added b/tests/difffile_fixtures/osctest/simple/.osc/_to_be_added
new file mode 100644
index 0000000..1f4923c
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_to_be_added
@@ -0,0 +1,3 @@
+toadd1
+replaced
+addedmissing
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/_to_be_deleted b/tests/difffile_fixtures/osctest/simple/.osc/_to_be_deleted
new file mode 100644
index 0000000..ebf038b
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/_to_be_deleted
@@ -0,0 +1 @@
+somefile
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/foo b/tests/difffile_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/merge b/tests/difffile_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/missing b/tests/difffile_fixtures/osctest/simple/.osc/missing
new file mode 100644
index 0000000..33e45d5
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/missing
@@ -0,0 +1 @@
+missing
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/nochange b/tests/difffile_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/replaced b/tests/difffile_fixtures/osctest/simple/.osc/replaced
new file mode 100644
index 0000000..7c3f1a8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/replaced
@@ -0,0 +1 @@
+yet another file
diff --git a/tests/difffile_fixtures/osctest/simple/.osc/somefile b/tests/difffile_fixtures/osctest/simple/.osc/somefile
new file mode 100644
index 0000000..2ef267e
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/.osc/somefile
@@ -0,0 +1 @@
+some content
diff --git a/tests/difffile_fixtures/osctest/simple/foo b/tests/difffile_fixtures/osctest/simple/foo
new file mode 100644
index 0000000..ad9621d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/foo
@@ -0,0 +1,5 @@
+<<<<<<< foo.mine
+This is no test.
+=======
+This is a simple test.
+>>>>>>> foo.r2
diff --git a/tests/difffile_fixtures/osctest/simple/merge b/tests/difffile_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/difffile_fixtures/osctest/simple/nochange b/tests/difffile_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/difffile_fixtures/osctest/simple/replaced b/tests/difffile_fixtures/osctest/simple/replaced
new file mode 100644
index 0000000..f479fb8
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/replaced
@@ -0,0 +1 @@
+foo replaced
diff --git a/tests/difffile_fixtures/osctest/simple/toadd1 b/tests/difffile_fixtures/osctest/simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/difffile_fixtures/osctest/simple/toadd2 b/tests/difffile_fixtures/osctest/simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/difffile_fixtures/osctest/simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/difffile_fixtures/testDiffRemoteDeletedLocalAdded_files b/tests/difffile_fixtures/testDiffRemoteDeletedLocalAdded_files
new file mode 100644
index 0000000..4aec6af
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteDeletedLocalAdded_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_binary b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_binary
new file mode 100644
index 0000000..188a937
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_binary
Binary files differ
diff --git a/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_files b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_files
new file mode 100644
index 0000000..5ac058d
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_files
@@ -0,0 +1,7 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="136a96e1470ec7424bc8ae47612977db" mtime="1282914026" name="foobar" size="14" />
+ <entry md5="9b55c93ffec5ef8850c84882de7ef6b5" mtime="1283242538" name="binary" size="7" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_foobar b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_foobar
new file mode 100644
index 0000000..e7856a6
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteExistingLocalNotExisting_foobar
@@ -0,0 +1,2 @@
+foobar
+barfoo
diff --git a/tests/difffile_fixtures/testDiffRemoteMissingLocalDeleted_files b/tests/difffile_fixtures/testDiffRemoteMissingLocalDeleted_files
new file mode 100644
index 0000000..3ceb10c
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteMissingLocalDeleted_files
@@ -0,0 +1,4 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteMissingLocalExisting_files b/tests/difffile_fixtures/testDiffRemoteMissingLocalExisting_files
new file mode 100644
index 0000000..204fdda
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteMissingLocalExisting_files
@@ -0,0 +1,4 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteModified_files b/tests/difffile_fixtures/testDiffRemoteModified_files
new file mode 100644
index 0000000..d0983af
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteModified_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="daafe513479072c5a942928d1850a939" mtime="1282908295" name="merge" size="35" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteModified_merge b/tests/difffile_fixtures/testDiffRemoteModified_merge
new file mode 100644
index 0000000..6236bd0
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteModified_merge
@@ -0,0 +1,3 @@
+Is it
+possible to
+merge this file?
diff --git a/tests/difffile_fixtures/testDiffRemoteNoChange_files b/tests/difffile_fixtures/testDiffRemoteNoChange_files
new file mode 100644
index 0000000..4aec6af
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteNoChange_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_binary b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_binary
new file mode 100644
index 0000000..5868978
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_binary
Binary files differ
diff --git a/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_files b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_files
new file mode 100644
index 0000000..054024f
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="3" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="3">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="b1b642cdbacf9956104f8565e297ed00" mtime="1283246089" name="binary" size="27" />
+</directory>
diff --git a/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_nochange b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/difffile_fixtures/testDiffRemoteUnchangedLocalModified_nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/init_package_fixtures/oscrc b/tests/init_package_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/init_package_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/init_project_fixtures/oscrc b/tests/init_project_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/init_project_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/osc b/tests/osc
new file mode 120000
index 0000000..b2faf3e
--- /dev/null
+++ b/tests/osc
@@ -0,0 +1 @@
+../osc \ No newline at end of file
diff --git a/tests/prdiff_fixtures/common-two-diff b/tests/prdiff_fixtures/common-two-diff
new file mode 100644
index 0000000..a06ddea
--- /dev/null
+++ b/tests/prdiff_fixtures/common-two-diff
@@ -0,0 +1,10 @@
+Index: common-two
+===================================================================
+--- common-two 2013-01-18 19:18:38.225983117 +0000
++++ common-two 2013-01-18 19:19:27.882082325 +0000
+@@ -1,4 +1,5 @@
+ line one
+ line two
+ line three
++an extra line
+ last line
diff --git a/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_apiurl b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_packages b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_packages
new file mode 100644
index 0000000..e1711ef
--- /dev/null
+++ b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_packages
@@ -0,0 +1,4 @@
+<project name="home:user:branches:some:project">
+ <package name="common-one" state=" " />
+ <package name="common-two" state=" " />
+</project> \ No newline at end of file
diff --git a/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_project b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_project
new file mode 100644
index 0000000..b83a395
--- /dev/null
+++ b/tests/prdiff_fixtures/home:user:branches:some:project/.osc/_project
@@ -0,0 +1 @@
+home:user:branches:some:project
diff --git a/tests/prdiff_fixtures/home:user:branches:some:project/common-two b/tests/prdiff_fixtures/home:user:branches:some:project/common-two
new file mode 100644
index 0000000..ade1e2d
--- /dev/null
+++ b/tests/prdiff_fixtures/home:user:branches:some:project/common-two
@@ -0,0 +1,5 @@
+line one
+line two
+line three
+an extra line
+last line \ No newline at end of file
diff --git a/tests/prdiff_fixtures/home:user:branches:some:project/directory b/tests/prdiff_fixtures/home:user:branches:some:project/directory
new file mode 100644
index 0000000..f29d454
--- /dev/null
+++ b/tests/prdiff_fixtures/home:user:branches:some:project/directory
@@ -0,0 +1,6 @@
+<directory count='4'>
+ <entry name="common-one"/>
+ <entry name="common-two"/>
+ <entry name="common-three"/>
+ <entry name="only-in-new"/>
+</directory>
diff --git a/tests/prdiff_fixtures/new:prj/common-two b/tests/prdiff_fixtures/new:prj/common-two
new file mode 100644
index 0000000..ade1e2d
--- /dev/null
+++ b/tests/prdiff_fixtures/new:prj/common-two
@@ -0,0 +1,5 @@
+line one
+line two
+line three
+an extra line
+last line \ No newline at end of file
diff --git a/tests/prdiff_fixtures/new:prj/directory b/tests/prdiff_fixtures/new:prj/directory
new file mode 100644
index 0000000..f29d454
--- /dev/null
+++ b/tests/prdiff_fixtures/new:prj/directory
@@ -0,0 +1,6 @@
+<directory count='4'>
+ <entry name="common-one"/>
+ <entry name="common-two"/>
+ <entry name="common-three"/>
+ <entry name="only-in-new"/>
+</directory>
diff --git a/tests/prdiff_fixtures/no-requests b/tests/prdiff_fixtures/no-requests
new file mode 100644
index 0000000..aef429f
--- /dev/null
+++ b/tests/prdiff_fixtures/no-requests
@@ -0,0 +1,2 @@
+<collection matches="0">
+</collection>
diff --git a/tests/prdiff_fixtures/old:prj/common-two b/tests/prdiff_fixtures/old:prj/common-two
new file mode 100644
index 0000000..48365a3
--- /dev/null
+++ b/tests/prdiff_fixtures/old:prj/common-two
@@ -0,0 +1,4 @@
+line one
+line two
+line three
+last line \ No newline at end of file
diff --git a/tests/prdiff_fixtures/old:prj/directory b/tests/prdiff_fixtures/old:prj/directory
new file mode 100644
index 0000000..a9db4b7
--- /dev/null
+++ b/tests/prdiff_fixtures/old:prj/directory
@@ -0,0 +1,6 @@
+<directory count='4'>
+ <entry name="common-one"/>
+ <entry name="common-two"/>
+ <entry name="common-three"/>
+ <entry name="only-in-old"/>
+</directory>
diff --git a/tests/prdiff_fixtures/oscrc b/tests/prdiff_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/prdiff_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/prdiff_fixtures/osctest/.osc/_apiurl b/tests/prdiff_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/prdiff_fixtures/osctest/.osc/_packages b/tests/prdiff_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..e1711ef
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/.osc/_packages
@@ -0,0 +1,4 @@
+<project name="home:user:branches:some:project">
+ <package name="common-one" state=" " />
+ <package name="common-two" state=" " />
+</project> \ No newline at end of file
diff --git a/tests/prdiff_fixtures/osctest/.osc/_project b/tests/prdiff_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83a395
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+home:user:branches:some:project
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_apiurl b/tests/prdiff_fixtures/osctest/common-one/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_files b/tests/prdiff_fixtures/osctest/common-one/.osc/_files
new file mode 100644
index 0000000..4b6dcca
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="common-one" rev="f53d033d63c3d6e9a8e4493225976122" srcmd5="f53d033d63c3d6e9a8e4493225976122">
+ <linkinfo baserev="896e6d6d675d03b6934946d03a976450" lsrcmd5="0cf460222270b58e2a9a3d695b1d945d" package="common-one" project="some:project" srcmd5="8c7ed3cf5ec0b4aa20ef159fd8c51b76" />
+ <entry md5="1a4c23ccf2eb12403acbfa3258233a9d" mtime="1352816081" name="common-one.spec" size="3457" />
+</directory>
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_meta b/tests/prdiff_fixtures/osctest/common-one/.osc/_meta
new file mode 100644
index 0000000..3804519
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_meta
@@ -0,0 +1,10 @@
+<package name="common-one" project="home:user:branches:some:project">
+ <title>blah</title>
+ <description>foo</description>
+ <debuginfo>
+ <enable repository="openSUSE_12.2"/>
+ <enable repository="openSUSE_Factory"/>
+ <enable repository="SLE_11_SP2"/>
+ </debuginfo>
+</package>
+
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_osclib_version b/tests/prdiff_fixtures/osctest/common-one/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_package b/tests/prdiff_fixtures/osctest/common-one/.osc/_package
new file mode 100644
index 0000000..089880f
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_package
@@ -0,0 +1 @@
+common-one \ No newline at end of file
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/_project b/tests/prdiff_fixtures/osctest/common-one/.osc/_project
new file mode 100644
index 0000000..b83a395
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/_project
@@ -0,0 +1 @@
+home:user:branches:some:project
diff --git a/tests/prdiff_fixtures/osctest/common-one/.osc/common-one.spec b/tests/prdiff_fixtures/osctest/common-one/.osc/common-one.spec
new file mode 100644
index 0000000..99bed78
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/.osc/common-one.spec
@@ -0,0 +1 @@
+contents are irrelevant
diff --git a/tests/prdiff_fixtures/osctest/common-one/common-one.spec b/tests/prdiff_fixtures/osctest/common-one/common-one.spec
new file mode 100644
index 0000000..99bed78
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-one/common-one.spec
@@ -0,0 +1 @@
+contents are irrelevant
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_apiurl b/tests/prdiff_fixtures/osctest/common-two/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_files b/tests/prdiff_fixtures/osctest/common-two/.osc/_files
new file mode 100644
index 0000000..63b65f2
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="common-two" rev="f53d033d63c3d6e9a8e4493225976122" srcmd5="f53d033d63c3d6e9a8e4493225976122">
+ <linkinfo baserev="896e6d6d675d03b6934946d03a976450" lsrcmd5="0cf460222270b58e2a9a3d695b1d945d" package="common-two" project="some:project" srcmd5="8c7ed3cf5ec0b4aa20ef159fd8c51b76" />
+ <entry md5="1a4c23ccf2eb12403acbfa3258233a9d" mtime="1352816081" name="common-two.spec" size="3457" />
+</directory>
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_meta b/tests/prdiff_fixtures/osctest/common-two/.osc/_meta
new file mode 100644
index 0000000..3d41ffd
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_meta
@@ -0,0 +1,10 @@
+<package name="common-two" project="home:user:branches:some:project">
+ <title>blah</title>
+ <description>foo</description>
+ <debuginfo>
+ <enable repository="openSUSE_12.2"/>
+ <enable repository="openSUSE_Factory"/>
+ <enable repository="SLE_11_SP2"/>
+ </debuginfo>
+</package>
+
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_osclib_version b/tests/prdiff_fixtures/osctest/common-two/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_package b/tests/prdiff_fixtures/osctest/common-two/.osc/_package
new file mode 100644
index 0000000..2ff3828
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_package
@@ -0,0 +1 @@
+common-two \ No newline at end of file
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/_project b/tests/prdiff_fixtures/osctest/common-two/.osc/_project
new file mode 100644
index 0000000..b83a395
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/_project
@@ -0,0 +1 @@
+home:user:branches:some:project
diff --git a/tests/prdiff_fixtures/osctest/common-two/.osc/common-two.spec b/tests/prdiff_fixtures/osctest/common-two/.osc/common-two.spec
new file mode 100644
index 0000000..99bed78
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/.osc/common-two.spec
@@ -0,0 +1 @@
+contents are irrelevant
diff --git a/tests/prdiff_fixtures/osctest/common-two/common-two.spec b/tests/prdiff_fixtures/osctest/common-two/common-two.spec
new file mode 100644
index 0000000..99bed78
--- /dev/null
+++ b/tests/prdiff_fixtures/osctest/common-two/common-two.spec
@@ -0,0 +1 @@
+contents are irrelevant
diff --git a/tests/prdiff_fixtures/request b/tests/prdiff_fixtures/request
new file mode 100644
index 0000000..2355cbb
--- /dev/null
+++ b/tests/prdiff_fixtures/request
@@ -0,0 +1,16 @@
+<collection matches="1">
+ <request id="148023">
+ <action type="submit">
+ <source project="home:user:branches:some:project" package="common-two" rev="7"/>
+ <target project="some:project" package="common-two"/>
+ <options>
+ <sourceupdate>update</sourceupdate>
+ </options>
+ </action>
+ <state name="new" who="user" when="2013-01-11T11:04:14">
+ <comment/>
+ </state>
+ <description>- Fix it to work
+- Improve support for something</description>
+ </request>
+</collection>
diff --git a/tests/prdiff_fixtures/some:project/.osc/_apiurl b/tests/prdiff_fixtures/some:project/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/prdiff_fixtures/some:project/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/prdiff_fixtures/some:project/.osc/_packages b/tests/prdiff_fixtures/some:project/.osc/_packages
new file mode 100644
index 0000000..c4c8b11
--- /dev/null
+++ b/tests/prdiff_fixtures/some:project/.osc/_packages
@@ -0,0 +1,4 @@
+<project name="some:project">
+ <package name="common-one" state=" " />
+ <package name="common-two" state=" " />
+</project> \ No newline at end of file
diff --git a/tests/prdiff_fixtures/some:project/.osc/_project b/tests/prdiff_fixtures/some:project/.osc/_project
new file mode 100644
index 0000000..f9a316e
--- /dev/null
+++ b/tests/prdiff_fixtures/some:project/.osc/_project
@@ -0,0 +1 @@
+some:project
diff --git a/tests/prdiff_fixtures/some:project/common-two b/tests/prdiff_fixtures/some:project/common-two
new file mode 100644
index 0000000..ade1e2d
--- /dev/null
+++ b/tests/prdiff_fixtures/some:project/common-two
@@ -0,0 +1,5 @@
+line one
+line two
+line three
+an extra line
+last line \ No newline at end of file
diff --git a/tests/prdiff_fixtures/some:project/directory b/tests/prdiff_fixtures/some:project/directory
new file mode 100644
index 0000000..f29d454
--- /dev/null
+++ b/tests/prdiff_fixtures/some:project/directory
@@ -0,0 +1,6 @@
+<directory count='4'>
+ <entry name="common-one"/>
+ <entry name="common-two"/>
+ <entry name="common-three"/>
+ <entry name="only-in-new"/>
+</directory>
diff --git a/tests/project_package_status_fixtures/oscrc b/tests/project_package_status_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/project_package_status_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/project_package_status_fixtures/osctest/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/.osc/_packages b/tests/project_package_status_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..2b174fd
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/.osc/_packages
@@ -0,0 +1,9 @@
+<project name="osctest">
+ <package name="conflict" state=" " />
+ <package name="simple" state=" " />
+ <package name="added" state="A" />
+ <package name="deleted" state="D" />
+ <package name="missing" state="!" />
+ <package name="added_deleted" state="A" />
+ <package name="deleted_deleted" state="D" />
+</project>
diff --git a/tests/project_package_status_fixtures/osctest/.osc/_project b/tests/project_package_status_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/added/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_files b/tests/project_package_status_fixtures/osctest/added/.osc/_files
new file mode 100644
index 0000000..9814121
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_files
@@ -0,0 +1 @@
+<directory />
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_osclib_version b/tests/project_package_status_fixtures/osctest/added/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_package b/tests/project_package_status_fixtures/osctest/added/.osc/_package
new file mode 100644
index 0000000..d5f7fc3
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_package
@@ -0,0 +1 @@
+added
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_project b/tests/project_package_status_fixtures/osctest/added/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/project_package_status_fixtures/osctest/added/.osc/_to_be_added b/tests/project_package_status_fixtures/osctest/added/.osc/_to_be_added
new file mode 100644
index 0000000..3e75765
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/.osc/_to_be_added
@@ -0,0 +1 @@
+new
diff --git a/tests/project_package_status_fixtures/osctest/added/exists b/tests/project_package_status_fixtures/osctest/added/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/exists
diff --git a/tests/project_package_status_fixtures/osctest/added/new b/tests/project_package_status_fixtures/osctest/added/new
new file mode 100644
index 0000000..3e75765
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/added/new
@@ -0,0 +1 @@
+new
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/conflict/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_files b/tests/project_package_status_fixtures/osctest/conflict/.osc/_files
new file mode 100644
index 0000000..141eaef
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="1282047303" name="conflict" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_in_conflict b/tests/project_package_status_fixtures/osctest/conflict/.osc/_in_conflict
new file mode 100644
index 0000000..9b1719f
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_in_conflict
@@ -0,0 +1 @@
+conflict
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_osclib_version b/tests/project_package_status_fixtures/osctest/conflict/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_package b/tests/project_package_status_fixtures/osctest/conflict/.osc/_package
new file mode 100644
index 0000000..9b1719f
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_package
@@ -0,0 +1 @@
+conflict
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/_project b/tests/project_package_status_fixtures/osctest/conflict/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/conflict b/tests/project_package_status_fixtures/osctest/conflict/.osc/conflict
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/conflict
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/project_package_status_fixtures/osctest/conflict/.osc/test b/tests/project_package_status_fixtures/osctest/conflict/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/conflict/conflict b/tests/project_package_status_fixtures/osctest/conflict/conflict
new file mode 100644
index 0000000..e47c5a6
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/conflict
@@ -0,0 +1 @@
+Inconflict
diff --git a/tests/project_package_status_fixtures/osctest/conflict/exists b/tests/project_package_status_fixtures/osctest/conflict/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/exists
diff --git a/tests/project_package_status_fixtures/osctest/conflict/test b/tests/project_package_status_fixtures/osctest/conflict/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/conflict/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/deleted/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_files b/tests/project_package_status_fixtures/osctest/deleted/.osc/_files
new file mode 100644
index 0000000..af92bf7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="1282047303" name="modified" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_osclib_version b/tests/project_package_status_fixtures/osctest/deleted/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_package b/tests/project_package_status_fixtures/osctest/deleted/.osc/_package
new file mode 100644
index 0000000..71779d2
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_package
@@ -0,0 +1 @@
+deleted
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_project b/tests/project_package_status_fixtures/osctest/deleted/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/_to_be_deleted b/tests/project_package_status_fixtures/osctest/deleted/.osc/_to_be_deleted
new file mode 100644
index 0000000..25fff4f
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+modified
+test
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/modified b/tests/project_package_status_fixtures/osctest/deleted/.osc/modified
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/modified
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/project_package_status_fixtures/osctest/deleted/.osc/test b/tests/project_package_status_fixtures/osctest/deleted/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/deleted/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/excluded/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/_files b/tests/project_package_status_fixtures/osctest/excluded/.osc/_files
new file mode 100644
index 0000000..af92bf7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/_files
@@ -0,0 +1,4 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="2abd19de6a38ff2890af64f453df96b1" mtime="1282047303" name="modified" size="22" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+</directory>
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/_osclib_version b/tests/project_package_status_fixtures/osctest/excluded/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/_package b/tests/project_package_status_fixtures/osctest/excluded/.osc/_package
new file mode 100644
index 0000000..bbde3dc
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/_package
@@ -0,0 +1 @@
+excluded
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/_project b/tests/project_package_status_fixtures/osctest/excluded/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/modified b/tests/project_package_status_fixtures/osctest/excluded/.osc/modified
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/modified
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/project_package_status_fixtures/osctest/excluded/.osc/test b/tests/project_package_status_fixtures/osctest/excluded/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/excluded/_linkerror b/tests/project_package_status_fixtures/osctest/excluded/_linkerror
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/_linkerror
diff --git a/tests/project_package_status_fixtures/osctest/excluded/dir/file b/tests/project_package_status_fixtures/osctest/excluded/dir/file
new file mode 100644
index 0000000..f73f309
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/dir/file
@@ -0,0 +1 @@
+file
diff --git a/tests/project_package_status_fixtures/osctest/excluded/exists b/tests/project_package_status_fixtures/osctest/excluded/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/exists
diff --git a/tests/project_package_status_fixtures/osctest/excluded/foo.orig b/tests/project_package_status_fixtures/osctest/excluded/foo.orig
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/foo.orig
diff --git a/tests/project_package_status_fixtures/osctest/excluded/modified b/tests/project_package_status_fixtures/osctest/excluded/modified
new file mode 100644
index 0000000..2e09960
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/modified
@@ -0,0 +1 @@
+modified
diff --git a/tests/project_package_status_fixtures/osctest/excluded/test b/tests/project_package_status_fixtures/osctest/excluded/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/excluded/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_apiurl b/tests/project_package_status_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_files b/tests/project_package_status_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..01e60f4
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,8 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="676513fde5797c3785164942c97dfec1" mtime="1283506309" name="missing" size="8" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="d8e8fca2dc0f896fd7cb4cb0031ba249" mtime="1283505591" name="test" size="5" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="100" skipped="true" />
+</directory>
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_osclib_version b/tests/project_package_status_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_package b/tests/project_package_status_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..ab23474
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_project b/tests/project_package_status_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_added b/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_added
new file mode 100644
index 0000000..f499143
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_added
@@ -0,0 +1,3 @@
+add
+missing
+missing_added
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_deleted b/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/foo b/tests/project_package_status_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/merge b/tests/project_package_status_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/missing b/tests/project_package_status_fixtures/osctest/simple/.osc/missing
new file mode 100644
index 0000000..33e45d5
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/missing
@@ -0,0 +1 @@
+missing
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/nochange b/tests/project_package_status_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/project_package_status_fixtures/osctest/simple/.osc/test b/tests/project_package_status_fixtures/osctest/simple/.osc/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/.osc/test
@@ -0,0 +1 @@
+test
diff --git a/tests/project_package_status_fixtures/osctest/simple/add b/tests/project_package_status_fixtures/osctest/simple/add
new file mode 100644
index 0000000..b242c36
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/add
@@ -0,0 +1 @@
+added file
diff --git a/tests/project_package_status_fixtures/osctest/simple/exists b/tests/project_package_status_fixtures/osctest/simple/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/exists
diff --git a/tests/project_package_status_fixtures/osctest/simple/missing b/tests/project_package_status_fixtures/osctest/simple/missing
new file mode 100644
index 0000000..feae347
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/missing
@@ -0,0 +1 @@
+replaced
diff --git a/tests/project_package_status_fixtures/osctest/simple/nochange b/tests/project_package_status_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..34d6872
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/nochange
@@ -0,0 +1 @@
+This file did change.
diff --git a/tests/project_package_status_fixtures/osctest/simple/test b/tests/project_package_status_fixtures/osctest/simple/test
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/project_package_status_fixtures/osctest/simple/test
@@ -0,0 +1 @@
+test
diff --git a/tests/repairwc_fixtures/oscrc b/tests/repairwc_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/repairwc_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/repairwc_fixtures/osctest/.osc/_apiurl b/tests/repairwc_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/.osc/_packages b/tests/repairwc_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/repairwc_fixtures/osctest/.osc/_project b/tests/repairwc_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/repairwc_fixtures/osctest/_packages b/tests/repairwc_fixtures/osctest/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_apiurl b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildconfig_prj_arch b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildconfig_prj_arch
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildconfig_prj_arch
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildinfo_prj_arch.xml b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildinfo_prj_arch.xml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_buildinfo_prj_arch.xml
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_files b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_files
new file mode 100644
index 0000000..d8e2ba4
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="buildfiles" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_in_conflict
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_in_conflict
@@ -0,0 +1 @@
+nochange
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_package b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_package
new file mode 100644
index 0000000..8c26334
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_package
@@ -0,0 +1 @@
+buildfiles
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_project b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/foo b/tests/repairwc_fixtures/osctest/buildfiles/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/merge b/tests/repairwc_fixtures/osctest/buildfiles/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/.osc/nochange b/tests/repairwc_fixtures/osctest/buildfiles/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/foobar b/tests/repairwc_fixtures/osctest/buildfiles/foobar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/foobar
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/merge b/tests/repairwc_fixtures/osctest/buildfiles/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/nochange b/tests/repairwc_fixtures/osctest/buildfiles/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/buildfiles/toadd1 b/tests/repairwc_fixtures/osctest/buildfiles/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/buildfiles/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_apiurl b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_apiurl
new file mode 100644
index 0000000..718a28b
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_apiurl
@@ -0,0 +1 @@
+urlwithoutprotocolandtld
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_files b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_files
new file mode 100644
index 0000000..15245ce
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_files
@@ -0,0 +1 @@
+<directory name="invalid_apiurl" rev="1" vrev="1" srcmd5="2738234914de5cc154b1494b1e98d940" />
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_meta b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_meta
new file mode 100644
index 0000000..13c5146
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_meta
@@ -0,0 +1,11 @@
+<package project="remote" name="foo">
+ <title>Title of New Package</title>
+ <description>
+LONG DESCRIPTION
+GOES
+HERE
+ </description>
+ <person userid="Admin" role="maintainer"/>
+ <person userid="Admin" role="bugowner"/>
+ <url>PUT_UPSTREAM_URL_HERE</url>
+</package>
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_package b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_package
new file mode 100644
index 0000000..2c2226b
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_package
@@ -0,0 +1 @@
+invalid_apiurl
diff --git a/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_project b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_project
new file mode 100644
index 0000000..9c998f7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/invalid_apiurl/.osc/_project
@@ -0,0 +1 @@
+remote
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_apiurl b/tests/repairwc_fixtures/osctest/multiple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_files b/tests/repairwc_fixtures/osctest/multiple/.osc/_files
new file mode 100644
index 0000000..8a96986
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="multiple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/multiple/.osc/_in_conflict
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_in_conflict
@@ -0,0 +1 @@
+nochange
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/multiple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_package b/tests/repairwc_fixtures/osctest/multiple/.osc/_package
new file mode 100644
index 0000000..5c4139d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_package
@@ -0,0 +1 @@
+multiple
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_project b/tests/repairwc_fixtures/osctest/multiple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_deleted
new file mode 100644
index 0000000..300a93b
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+foo
+nofilesentry
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/foo b/tests/repairwc_fixtures/osctest/multiple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/multiple/.osc/unknown_file b/tests/repairwc_fixtures/osctest/multiple/.osc/unknown_file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/.osc/unknown_file
diff --git a/tests/repairwc_fixtures/osctest/multiple/foobar b/tests/repairwc_fixtures/osctest/multiple/foobar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/foobar
diff --git a/tests/repairwc_fixtures/osctest/multiple/merge b/tests/repairwc_fixtures/osctest/multiple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/multiple/nochange b/tests/repairwc_fixtures/osctest/multiple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/multiple/toadd1 b/tests/repairwc_fixtures/osctest/multiple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/multiple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_files b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_files
new file mode 100644
index 0000000..e9158ba
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="noapiurl" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_in_conflict
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_in_conflict
@@ -0,0 +1 @@
+nochange
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_package b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_package
new file mode 100644
index 0000000..14aa43c
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_package
@@ -0,0 +1 @@
+noapiurl
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_project b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/foo b/tests/repairwc_fixtures/osctest/noapiurl/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/merge b/tests/repairwc_fixtures/osctest/noapiurl/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/.osc/nochange b/tests/repairwc_fixtures/osctest/noapiurl/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/foobar b/tests/repairwc_fixtures/osctest/noapiurl/foobar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/foobar
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/merge b/tests/repairwc_fixtures/osctest/noapiurl/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/nochange b/tests/repairwc_fixtures/osctest/noapiurl/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/noapiurl/toadd1 b/tests/repairwc_fixtures/osctest/noapiurl/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/noapiurl/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_files b/tests/repairwc_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_package b/tests/repairwc_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_project b/tests/repairwc_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/foo b/tests/repairwc_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/merge b/tests/repairwc_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple/.osc/nochange b/tests/repairwc_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple/merge b/tests/repairwc_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple/nochange b/tests/repairwc_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple/toadd1 b/tests/repairwc_fixtures/osctest/simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple/toadd2 b/tests/repairwc_fixtures/osctest/simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple1/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_files b/tests/repairwc_fixtures/osctest/simple1/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple1/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_package b/tests/repairwc_fixtures/osctest/simple1/.osc/_package
new file mode 100644
index 0000000..e2464cd
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_package
@@ -0,0 +1 @@
+simple1
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_project b/tests/repairwc_fixtures/osctest/simple1/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple1/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/merge b/tests/repairwc_fixtures/osctest/simple1/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple1/.osc/nochange b/tests/repairwc_fixtures/osctest/simple1/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple1/merge b/tests/repairwc_fixtures/osctest/simple1/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple1/nochange b/tests/repairwc_fixtures/osctest/simple1/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple1/toadd1 b/tests/repairwc_fixtures/osctest/simple1/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple1/toadd2 b/tests/repairwc_fixtures/osctest/simple1/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple1/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple2/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_files b/tests/repairwc_fixtures/osctest/simple2/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple2/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_package b/tests/repairwc_fixtures/osctest/simple2/.osc/_package
new file mode 100644
index 0000000..e268fff
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_package
@@ -0,0 +1 @@
+simple2
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_project b/tests/repairwc_fixtures/osctest/simple2/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple2/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/foo b/tests/repairwc_fixtures/osctest/simple2/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/merge b/tests/repairwc_fixtures/osctest/simple2/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/nochange b/tests/repairwc_fixtures/osctest/simple2/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple2/.osc/somefile b/tests/repairwc_fixtures/osctest/simple2/.osc/somefile
new file mode 100644
index 0000000..ebf038b
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/.osc/somefile
@@ -0,0 +1 @@
+somefile
diff --git a/tests/repairwc_fixtures/osctest/simple2/merge b/tests/repairwc_fixtures/osctest/simple2/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple2/nochange b/tests/repairwc_fixtures/osctest/simple2/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple2/toadd1 b/tests/repairwc_fixtures/osctest/simple2/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple2/toadd2 b/tests/repairwc_fixtures/osctest/simple2/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple2/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple3/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_files b/tests/repairwc_fixtures/osctest/simple3/.osc/_files
new file mode 100644
index 0000000..0479b49
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple3" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple3/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_package b/tests/repairwc_fixtures/osctest/simple3/.osc/_package
new file mode 100644
index 0000000..3d7b9c9
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_package
@@ -0,0 +1 @@
+simple3
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_project b/tests/repairwc_fixtures/osctest/simple3/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_added
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_added
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/foo b/tests/repairwc_fixtures/osctest/simple3/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/merge b/tests/repairwc_fixtures/osctest/simple3/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/nochange b/tests/repairwc_fixtures/osctest/simple3/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple3/.osc/toadd1 b/tests/repairwc_fixtures/osctest/simple3/.osc/toadd1
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/.osc/toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple3/merge b/tests/repairwc_fixtures/osctest/simple3/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple3/nochange b/tests/repairwc_fixtures/osctest/simple3/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple3/toadd1 b/tests/repairwc_fixtures/osctest/simple3/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple3/toadd2 b/tests/repairwc_fixtures/osctest/simple3/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple3/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple4/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_files b/tests/repairwc_fixtures/osctest/simple4/.osc/_files
new file mode 100644
index 0000000..9fa8a9f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="working_nonempty" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple4/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_package b/tests/repairwc_fixtures/osctest/simple4/.osc/_package
new file mode 100644
index 0000000..6ece159
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_package
@@ -0,0 +1 @@
+working_nonempty
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_project b/tests/repairwc_fixtures/osctest/simple4/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple4/.osc/_to_be_deleted
new file mode 100644
index 0000000..6db8a6f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+foo
+remove
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/foo b/tests/repairwc_fixtures/osctest/simple4/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/merge b/tests/repairwc_fixtures/osctest/simple4/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple4/.osc/nochange b/tests/repairwc_fixtures/osctest/simple4/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple4/merge b/tests/repairwc_fixtures/osctest/simple4/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple4/nochange b/tests/repairwc_fixtures/osctest/simple4/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple4/toadd1 b/tests/repairwc_fixtures/osctest/simple4/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple4/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple5/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_files b/tests/repairwc_fixtures/osctest/simple5/.osc/_files
new file mode 100644
index 0000000..9fa8a9f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="working_nonempty" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/simple5/.osc/_in_conflict
new file mode 100644
index 0000000..9b1719f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_in_conflict
@@ -0,0 +1 @@
+conflict
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple5/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_package b/tests/repairwc_fixtures/osctest/simple5/.osc/_package
new file mode 100644
index 0000000..6ece159
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_package
@@ -0,0 +1 @@
+working_nonempty
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_project b/tests/repairwc_fixtures/osctest/simple5/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple5/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/foo b/tests/repairwc_fixtures/osctest/simple5/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/merge b/tests/repairwc_fixtures/osctest/simple5/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple5/.osc/nochange b/tests/repairwc_fixtures/osctest/simple5/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple5/merge b/tests/repairwc_fixtures/osctest/simple5/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple5/nochange b/tests/repairwc_fixtures/osctest/simple5/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple5/toadd1 b/tests/repairwc_fixtures/osctest/simple5/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple5/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple6/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_files b/tests/repairwc_fixtures/osctest/simple6/.osc/_files
new file mode 100644
index 0000000..65eb184
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple6" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple6/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_package b/tests/repairwc_fixtures/osctest/simple6/.osc/_package
new file mode 100644
index 0000000..29a2746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_package
@@ -0,0 +1 @@
+simple6
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_project b/tests/repairwc_fixtures/osctest/simple6/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/merge b/tests/repairwc_fixtures/osctest/simple6/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple6/.osc/nochange b/tests/repairwc_fixtures/osctest/simple6/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple6/merge b/tests/repairwc_fixtures/osctest/simple6/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple6/nochange b/tests/repairwc_fixtures/osctest/simple6/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple6/toadd1 b/tests/repairwc_fixtures/osctest/simple6/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple6/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple7/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_files b/tests/repairwc_fixtures/osctest/simple7/.osc/_files
new file mode 100644
index 0000000..9ae3788
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="simple7" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="42" skipped="true" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/simple7/.osc/_in_conflict
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_in_conflict
@@ -0,0 +1 @@
+nochange
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple7/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_package b/tests/repairwc_fixtures/osctest/simple7/.osc/_package
new file mode 100644
index 0000000..c0cec07
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_package
@@ -0,0 +1 @@
+simple7
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_project b/tests/repairwc_fixtures/osctest/simple7/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/foo b/tests/repairwc_fixtures/osctest/simple7/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/merge b/tests/repairwc_fixtures/osctest/simple7/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple7/.osc/nochange b/tests/repairwc_fixtures/osctest/simple7/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple7/foobar b/tests/repairwc_fixtures/osctest/simple7/foobar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/foobar
diff --git a/tests/repairwc_fixtures/osctest/simple7/merge b/tests/repairwc_fixtures/osctest/simple7/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple7/nochange b/tests/repairwc_fixtures/osctest/simple7/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple7/toadd1 b/tests/repairwc_fixtures/osctest/simple7/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple7/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_apiurl b/tests/repairwc_fixtures/osctest/simple8/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_files b/tests/repairwc_fixtures/osctest/simple8/.osc/_files
new file mode 100644
index 0000000..cd725ff
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="simple8" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="42" skipped="true" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/simple8/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_package b/tests/repairwc_fixtures/osctest/simple8/.osc/_package
new file mode 100644
index 0000000..fc76adf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_package
@@ -0,0 +1 @@
+simple8
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_project b/tests/repairwc_fixtures/osctest/simple8/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/foo b/tests/repairwc_fixtures/osctest/simple8/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/merge b/tests/repairwc_fixtures/osctest/simple8/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/nochange b/tests/repairwc_fixtures/osctest/simple8/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/simple8/.osc/skipped b/tests/repairwc_fixtures/osctest/simple8/.osc/skipped
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/.osc/skipped
diff --git a/tests/repairwc_fixtures/osctest/simple8/merge b/tests/repairwc_fixtures/osctest/simple8/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/simple8/nochange b/tests/repairwc_fixtures/osctest/simple8/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/simple8/toadd1 b/tests/repairwc_fixtures/osctest/simple8/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/simple8/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/osctest/working_empty/.osc/_apiurl b/tests/repairwc_fixtures/osctest/working_empty/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_empty/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/working_empty/.osc/_files b/tests/repairwc_fixtures/osctest/working_empty/.osc/_files
new file mode 100644
index 0000000..9814121
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_empty/.osc/_files
@@ -0,0 +1 @@
+<directory />
diff --git a/tests/repairwc_fixtures/osctest/working_empty/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/working_empty/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_empty/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/working_empty/.osc/_package b/tests/repairwc_fixtures/osctest/working_empty/.osc/_package
new file mode 100644
index 0000000..4b1dcd1
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_empty/.osc/_package
@@ -0,0 +1 @@
+working_empty
diff --git a/tests/repairwc_fixtures/osctest/working_empty/.osc/_project b/tests/repairwc_fixtures/osctest/working_empty/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_empty/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_apiurl b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_files b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_files
new file mode 100644
index 0000000..9fa8a9f
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="working_nonempty" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_in_conflict b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_in_conflict
new file mode 100644
index 0000000..55aa746
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_in_conflict
@@ -0,0 +1 @@
+nochange
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_osclib_version b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_package b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_package
new file mode 100644
index 0000000..6ece159
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_package
@@ -0,0 +1 @@
+working_nonempty
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_project b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_added b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_added
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_added
@@ -0,0 +1 @@
+foobar
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_deleted b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_deleted
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/_to_be_deleted
@@ -0,0 +1 @@
+foo
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/foo b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/merge b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/.osc/nochange b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/foobar b/tests/repairwc_fixtures/osctest/working_nonempty/foobar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/foobar
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/merge b/tests/repairwc_fixtures/osctest/working_nonempty/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/nochange b/tests/repairwc_fixtures/osctest/working_nonempty/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/repairwc_fixtures/osctest/working_nonempty/toadd1 b/tests/repairwc_fixtures/osctest/working_nonempty/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/repairwc_fixtures/osctest/working_nonempty/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_apiurl b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_apiurl
new file mode 100644
index 0000000..c2401e4
--- /dev/null
+++ b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_apiurl
@@ -0,0 +1 @@
+noschemeandnotld
diff --git a/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_packages b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_packages
new file mode 100644
index 0000000..9b61f30
--- /dev/null
+++ b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_packages
@@ -0,0 +1 @@
+<project name="prj_noapiurl" />
diff --git a/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_project b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_project
new file mode 100644
index 0000000..d3dc1c2
--- /dev/null
+++ b/tests/repairwc_fixtures/prj_invalidapiurl/.osc/_project
@@ -0,0 +1 @@
+prj_invalidapiurl
diff --git a/tests/repairwc_fixtures/prj_noapiurl/.osc/_packages b/tests/repairwc_fixtures/prj_noapiurl/.osc/_packages
new file mode 100644
index 0000000..9b61f30
--- /dev/null
+++ b/tests/repairwc_fixtures/prj_noapiurl/.osc/_packages
@@ -0,0 +1 @@
+<project name="prj_noapiurl" />
diff --git a/tests/repairwc_fixtures/prj_noapiurl/.osc/_project b/tests/repairwc_fixtures/prj_noapiurl/.osc/_project
new file mode 100644
index 0000000..08c78d8
--- /dev/null
+++ b/tests/repairwc_fixtures/prj_noapiurl/.osc/_project
@@ -0,0 +1 @@
+prj_noapiurl
diff --git a/tests/request_fixtures/oscrc b/tests/request_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/request_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/request_fixtures/test_read_request1.xml b/tests/request_fixtures/test_read_request1.xml
new file mode 100644
index 0000000..af69950
--- /dev/null
+++ b/tests/request_fixtures/test_read_request1.xml
@@ -0,0 +1,18 @@
+<request id="42">
+ <action type="submit">
+ <source package="bar" project="foo" rev="1" />
+ <target package="bar" project="foobar" />
+ </action>
+ <action type="delete">
+ <target project="deleteme" />
+ </action>
+ <state name="accepted" when="2010-12-27T01:36:29" who="user1" />
+ <history when="2010-12-13T13:02:03" who="creator">
+ <description>Create Request</description>
+ <comment>foobar</comment>
+ </history>
+ <title>title of the request</title>
+ <description>this is a
+very long
+description</description>
+</request>
diff --git a/tests/request_fixtures/test_read_request2.xml b/tests/request_fixtures/test_read_request2.xml
new file mode 100644
index 0000000..2916955
--- /dev/null
+++ b/tests/request_fixtures/test_read_request2.xml
@@ -0,0 +1,21 @@
+<request id="123">
+ <action type="submit">
+ <source package="abc" project="xyz" />
+ <options>
+ <sourceupdate>cleanup</sourceupdate>
+ <updatelink>1</updatelink>
+ </options>
+ </action>
+ <action type="add_role">
+ <target project="home:foo" />
+ <person name="bar" role="maintainer" />
+ <group name="groupxyz" role="reader" />
+ </action>
+ <state name="review" when="2010-12-27T01:36:29" who="abc" />
+ <review by_group="group1" state="new" when="2010-12-28T00:11:22" who="abc">
+ <comment>review start</comment>
+ </review>
+ <history when="2010-12-11T00:00:00" who="creator">
+ <description>Created request</description>
+ </history>
+</request>
diff --git a/tests/request_fixtures/test_request_list_view1.xml b/tests/request_fixtures/test_request_list_view1.xml
new file mode 100644
index 0000000..a0117b2
--- /dev/null
+++ b/tests/request_fixtures/test_request_list_view1.xml
@@ -0,0 +1,36 @@
+<request id="62">
+ <action type="set_bugowner">
+ <target project="foo" />
+ <person name="buguser" />
+ </action>
+ <action type="add_role">
+ <target project="foobar" />
+ <person name="xyz" role="maintainer" />
+ <group name="group1" role="reader" />
+ </action>
+ <action type="add_role">
+ <target project="foo" package="bar" />
+ <person name="abc" role="reviewer" />
+ </action>
+ <action type="change_devel">
+ <source project="devprj" package="devpkg" />
+ <target project="foo" package="bar" />
+ </action>
+ <action type="submit">
+ <source project="srcprj" package="srcpackage" />
+ <target project="tgtprj" package="tgtpackage" />
+ </action>
+ <action type="submit">
+ <source project="foo" package="bar" />
+ <target project="baz" />
+ </action>
+ <action type="delete">
+ <target project="deleteme" />
+ </action>
+ <action type="delete">
+ <target project="foo" package="bar" />
+ </action>
+ <state name="new" when="2010-12-29T14:57:25" who="Admin">
+ <comment></comment>
+ </state>
+</request>
diff --git a/tests/request_fixtures/test_request_list_view2.xml b/tests/request_fixtures/test_request_list_view2.xml
new file mode 100644
index 0000000..ae9213a
--- /dev/null
+++ b/tests/request_fixtures/test_request_list_view2.xml
@@ -0,0 +1,18 @@
+<request id="21">
+ <action type="set_bugowner">
+ <target project="foo" />
+ <person name="buguser" />
+ </action>
+ <state name="accepted" when="2010-12-29T16:37:45" who="foobar" />
+ <history when="2010-12-28T16:37:45" who="user" >
+ <description>Created Request</description>
+ </history>
+ <history when="2010-12-28T18:37:45" who="foobar" >
+ <description>Review Approved</description>
+ </history>
+ <description>This is
+a simple request with a lot of ... ... text and other stuff. This request also contains a
+description. This is useful to
+describe the request. blabla
+blabla</description>
+</request>
diff --git a/tests/request_fixtures/test_request_str1.xml b/tests/request_fixtures/test_request_str1.xml
new file mode 100644
index 0000000..ee5aa9b
--- /dev/null
+++ b/tests/request_fixtures/test_request_str1.xml
@@ -0,0 +1,30 @@
+<request id="123">
+ <action type="submit">
+ <source package="abc" project="xyz" />
+ <target project="foo" />
+ <options>
+ <sourceupdate>cleanup</sourceupdate>
+ <updatelink>1</updatelink>
+ </options>
+ </action>
+ <action type="add_role">
+ <target project="home:foo" />
+ <person name="bar" role="maintainer" />
+ <group name="groupxyz" role="reader" />
+ </action>
+ <state name="review" when="2010-12-27T01:36:29" who="abc">
+ <comment>currently in review</comment>
+ </state>
+ <review by_group="group1" state="new" when="2010-12-28T00:11:22" who="abc">
+ <comment>review start</comment>
+ </review>
+ <review by_group="group1" state="accepted" when="2010-12-29T00:11:22" who="abc">
+ <comment>accepted</comment>
+ </review>
+ <history name="new" when="2010-12-11T00:00:00" who="creator" />
+ <history name="revoked" when="2010-12-12T00:00:00" who="creator" />
+ <description>just a samll description
+in order to describe this
+request - blablabla
+test.</description>
+</request>
diff --git a/tests/revertfile_fixtures/oscrc b/tests/revertfile_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/revertfile_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/revertfile_fixtures/osctest/.osc/_apiurl b/tests/revertfile_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/revertfile_fixtures/osctest/.osc/_packages b/tests/revertfile_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/revertfile_fixtures/osctest/.osc/_project b/tests/revertfile_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_apiurl b/tests/revertfile_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_files b/tests/revertfile_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..5dbd576
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,10 @@
+<directory name="conflict" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="eb9c2bf0eb63f3a7bc0ea37ef18aeba5" mtime="1282730880" name="somefile" size="13" />
+ <entry md5="81be947db54c2e225dc8eacce64d8a4a" mtime="1282731457" name="replaced" size="17" />
+ <entry md5="676513fde5797c3785164942c97dfec1" mtime="1282731738" name="missing" size="8" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="deleted" size="9" />
+ <entry md5="ffffffffffffffffffffffffffffffff" mtime="1111111111" name="skipped" size="12" skipped="true" />
+</directory>
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_in_conflict b/tests/revertfile_fixtures/osctest/simple/.osc/_in_conflict
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_in_conflict
@@ -0,0 +1 @@
+foo
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_osclib_version b/tests/revertfile_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_package b/tests/revertfile_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_project b/tests/revertfile_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_added b/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_added
new file mode 100644
index 0000000..1f4923c
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_added
@@ -0,0 +1,3 @@
+toadd1
+replaced
+addedmissing
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_deleted b/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_deleted
new file mode 100644
index 0000000..08f95e5
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+somefile
+deleted
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/deleted b/tests/revertfile_fixtures/osctest/simple/.osc/deleted
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/deleted
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/foo b/tests/revertfile_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/merge b/tests/revertfile_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/missing b/tests/revertfile_fixtures/osctest/simple/.osc/missing
new file mode 100644
index 0000000..33e45d5
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/missing
@@ -0,0 +1 @@
+missing
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/nochange b/tests/revertfile_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/replaced b/tests/revertfile_fixtures/osctest/simple/.osc/replaced
new file mode 100644
index 0000000..7c3f1a8
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/replaced
@@ -0,0 +1 @@
+yet another file
diff --git a/tests/revertfile_fixtures/osctest/simple/.osc/somefile b/tests/revertfile_fixtures/osctest/simple/.osc/somefile
new file mode 100644
index 0000000..2ef267e
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/.osc/somefile
@@ -0,0 +1 @@
+some content
diff --git a/tests/revertfile_fixtures/osctest/simple/foo b/tests/revertfile_fixtures/osctest/simple/foo
new file mode 100644
index 0000000..ad9621d
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/foo
@@ -0,0 +1,5 @@
+<<<<<<< foo.mine
+This is no test.
+=======
+This is a simple test.
+>>>>>>> foo.r2
diff --git a/tests/revertfile_fixtures/osctest/simple/merge b/tests/revertfile_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/revertfile_fixtures/osctest/simple/nochange b/tests/revertfile_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/revertfile_fixtures/osctest/simple/replaced b/tests/revertfile_fixtures/osctest/simple/replaced
new file mode 100644
index 0000000..f479fb8
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/replaced
@@ -0,0 +1 @@
+foo replaced
diff --git a/tests/revertfile_fixtures/osctest/simple/toadd1 b/tests/revertfile_fixtures/osctest/simple/toadd1
new file mode 100644
index 0000000..1592423
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/toadd1
@@ -0,0 +1 @@
+toadd1
diff --git a/tests/revertfile_fixtures/osctest/simple/toadd2 b/tests/revertfile_fixtures/osctest/simple/toadd2
new file mode 100644
index 0000000..6f1ab97
--- /dev/null
+++ b/tests/revertfile_fixtures/osctest/simple/toadd2
@@ -0,0 +1 @@
+toadd2
diff --git a/tests/setlinkrev_fixtures/expandedsrc_filesremote b/tests/setlinkrev_fixtures/expandedsrc_filesremote
new file mode 100644
index 0000000..ad4d2a6
--- /dev/null
+++ b/tests/setlinkrev_fixtures/expandedsrc_filesremote
@@ -0,0 +1,5 @@
+<directory name="srcpkg" rev="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" srcmd5="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" vrev="1">
+ <linkinfo project="srcsrcprj" package="srcsrcpkg" srcmd5="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" baserev="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" xsrcmd5="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" lsrcmd5="cccccccccccccccccccccccccccccccc" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="_link" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+</directory>
diff --git a/tests/setlinkrev_fixtures/link_with_rev b/tests/setlinkrev_fixtures/link_with_rev
new file mode 100644
index 0000000..877aa3f
--- /dev/null
+++ b/tests/setlinkrev_fixtures/link_with_rev
@@ -0,0 +1 @@
+<link package="srcpkg" project="srcprj" rev="7" />
diff --git a/tests/setlinkrev_fixtures/md5_rev_link b/tests/setlinkrev_fixtures/md5_rev_link
new file mode 100644
index 0000000..3725d01
--- /dev/null
+++ b/tests/setlinkrev_fixtures/md5_rev_link
@@ -0,0 +1 @@
+<link package="srcpkg" project="srcprj" rev="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" vrev="1" />
diff --git a/tests/setlinkrev_fixtures/noproject_link b/tests/setlinkrev_fixtures/noproject_link
new file mode 100644
index 0000000..9f45c4f
--- /dev/null
+++ b/tests/setlinkrev_fixtures/noproject_link
@@ -0,0 +1 @@
+<link package="srcpkg" />
diff --git a/tests/setlinkrev_fixtures/oscrc b/tests/setlinkrev_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/setlinkrev_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/setlinkrev_fixtures/rev_link b/tests/setlinkrev_fixtures/rev_link
new file mode 100644
index 0000000..1c16c99
--- /dev/null
+++ b/tests/setlinkrev_fixtures/rev_link
@@ -0,0 +1 @@
+<link package="srcpkg" project="srcprj" rev="42" />
diff --git a/tests/setlinkrev_fixtures/simple_filesremote b/tests/setlinkrev_fixtures/simple_filesremote
new file mode 100644
index 0000000..0e5319f
--- /dev/null
+++ b/tests/setlinkrev_fixtures/simple_filesremote
@@ -0,0 +1,4 @@
+<directory name="srcpkg" rev="42" srcmd5="ffffffffffffffffffffffffffffffff" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+</directory>
diff --git a/tests/setlinkrev_fixtures/simple_link b/tests/setlinkrev_fixtures/simple_link
new file mode 100644
index 0000000..20db65a
--- /dev/null
+++ b/tests/setlinkrev_fixtures/simple_link
@@ -0,0 +1 @@
+<link package="srcpkg" project="srcprj" />
diff --git a/tests/suite.py b/tests/suite.py
new file mode 100644
index 0000000..0373e24
--- /dev/null
+++ b/tests/suite.py
@@ -0,0 +1,48 @@
+import os.path
+import sys
+import unittest
+
+try:
+ import xmlrunner # JUnit like XML reporting
+ have_xmlrunner = True
+except ImportError:
+ have_xmlrunner = False
+
+import test_update
+import test_addfiles
+import test_deletefiles
+import test_revertfiles
+import test_difffiles
+import test_init_package
+import test_init_project
+import test_commit
+import test_repairwc
+import test_package_status
+import test_project_status
+import test_request
+import test_setlinkrev
+import test_prdiff
+import test_conf
+
+suite = unittest.TestSuite()
+suite.addTests(test_addfiles.suite())
+suite.addTests(test_deletefiles.suite())
+suite.addTests(test_revertfiles.suite())
+suite.addTests(test_update.suite())
+suite.addTests(test_difffiles.suite())
+suite.addTests(test_init_package.suite())
+suite.addTests(test_init_project.suite())
+suite.addTests(test_commit.suite())
+suite.addTests(test_repairwc.suite())
+suite.addTests(test_package_status.suite())
+suite.addTests(test_project_status.suite())
+suite.addTests(test_request.suite())
+suite.addTests(test_setlinkrev.suite())
+suite.addTests(test_prdiff.suite())
+suite.addTests(test_conf.suite())
+
+if have_xmlrunner:
+ result = xmlrunner.XMLTestRunner(output=os.path.join(os.getcwd(), 'junit-xml-results')).run(suite)
+else:
+ result = unittest.TextTestRunner(verbosity=1).run(suite)
+sys.exit(not result.wasSuccessful())
diff --git a/tests/test_addfiles.py b/tests/test_addfiles.py
new file mode 100644
index 0000000..129bc4d
--- /dev/null
+++ b/tests/test_addfiles.py
@@ -0,0 +1,85 @@
+import osc.core
+import osc.oscerr
+import os
+import sys
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'addfile_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestAddFiles)
+
+class TestAddFiles(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def testSimpleAdd(self):
+ """add one file ('toadd1') to the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.addfile('toadd1')
+ exp = 'A toadd1\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1')))
+ self._check_status(p, 'toadd1', 'A')
+ self._check_addlist('toadd1\n')
+
+ def testSimpleMultipleAdd(self):
+ """add multiple files ('toadd1', 'toadd2') to the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.addfile('toadd1')
+ p.addfile('toadd2')
+ exp = 'A toadd1\nA toadd2\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd2')))
+ self._check_status(p, 'toadd1', 'A')
+ self._check_status(p, 'toadd2', 'A')
+ self._check_addlist('toadd1\ntoadd2\n')
+
+ def testAddVersionedFile(self):
+ """add a versioned file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self.assertRaises(osc.oscerr.PackageFileConflict, p.addfile, 'merge')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self._check_status(p, 'merge', ' ')
+
+ def testAddUnversionedFileTwice(self):
+ """add the same file twice"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.addfile('toadd1')
+ self.assertRaises(osc.oscerr.PackageFileConflict, p.addfile, 'toadd1')
+ exp = 'A toadd1\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1')))
+ self._check_status(p, 'toadd1', 'A')
+ self._check_addlist('toadd1\n')
+
+ def testReplace(self):
+ """replace a deleted file ('foo')"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ open('foo', 'w').write('replaced file\n')
+ p.addfile('foo')
+ exp = 'A foo\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertNotEqual(open(os.path.join('.osc', 'foo'), 'r').read(), 'replaced file\n')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_status(p, 'foo', 'R')
+ self._check_addlist('foo\n')
+
+ def testAddNonExistentFile(self):
+ """add a non existent file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self.assertRaises(osc.oscerr.OscIOError, p.addfile, 'doesnotexist')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_commit.py b/tests/test_commit.py
new file mode 100644
index 0000000..02baa85
--- /dev/null
+++ b/tests/test_commit.py
@@ -0,0 +1,317 @@
+import osc.core
+import osc.oscerr
+import os
+import sys
+from common import GET, PUT, POST, DELETE, OscTestCase
+from xml.etree import cElementTree as ET
+try:
+ from urllib.error import HTTPError
+except ImportError:
+ #python 2.x
+ from urllib2 import HTTPError
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'commit_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestCommit)
+
+rev_dummy = '<revision rev="repository">\n <srcmd5>empty</srcmd5>\n</revision>'
+
+class TestCommit(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ @GET('http://localhost/source/osctest/simple?rev=latest', file='testSimple_filesremote')
+ @POST('http://localhost/source/osctest/simple?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/simple?comment=&cmd=commitfilelist&user=Admin',
+ file='testSimple_missingfilelist', expfile='testSimple_lfilelist')
+ @PUT('http://localhost/source/osctest/simple/nochange?rev=repository',
+ exp='This file didn\'t change but\nis modified.\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/simple?comment=&cmd=commitfilelist&user=Admin',
+ file='testSimple_cfilesremote', expfile='testSimple_lfilelist')
+ def test_simple(self):
+ """a simple commit (only one modified file)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Sending nochange\nTransmitting file data .\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testSimple_cfilesremote')
+ self.assertTrue(os.path.exists('nochange'))
+ self.assertEqual(open('nochange', 'r').read(), open(os.path.join('.osc', 'nochange'), 'r').read())
+ self._check_status(p, 'nochange', ' ')
+ self._check_status(p, 'foo', ' ')
+ self._check_status(p, 'merge', ' ')
+
+ @GET('http://localhost/source/osctest/add?rev=latest', file='testAddfile_filesremote')
+ @POST('http://localhost/source/osctest/add?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/add?comment=&cmd=commitfilelist&user=Admin',
+ file='testAddfile_missingfilelist', expfile='testAddfile_lfilelist')
+ @PUT('http://localhost/source/osctest/add/add?rev=repository',
+ exp='added file\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/add?comment=&cmd=commitfilelist&user=Admin',
+ file='testAddfile_cfilesremote', expfile='testAddfile_lfilelist')
+ def test_addfile(self):
+ """commit a new file"""
+ self._change_to_pkg('add')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Sending add\nTransmitting file data .\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testAddfile_cfilesremote')
+ self.assertTrue(os.path.exists('add'))
+ self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'add'), 'r').read())
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self._check_status(p, 'add', ' ')
+ self._check_status(p, 'foo', ' ')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'nochange', ' ')
+
+ @GET('http://localhost/source/osctest/delete?rev=latest', file='testDeletefile_filesremote')
+ @POST('http://localhost/source/osctest/delete?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/delete?comment=&cmd=commitfilelist&user=Admin',
+ file='testDeletefile_cfilesremote', expfile='testDeletefile_lfilelist')
+ def test_deletefile(self):
+ """delete a file"""
+ self._change_to_pkg('delete')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Deleting nochange\nTransmitting file data \nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testDeletefile_cfilesremote')
+ self.assertFalse(os.path.exists('nochange'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'nochange')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_status(p, 'foo', ' ')
+ self._check_status(p, 'merge', ' ')
+
+ @GET('http://localhost/source/osctest/conflict?rev=latest', file='testConflictfile_filesremote')
+ @POST('http://localhost/source/osctest/conflict?cmd=getprojectservices',
+ exp='', text='<services />')
+ def test_conflictfile(self):
+ """package has a file which is in conflict state"""
+ self._change_to_pkg('conflict')
+ ret = osc.core.Package('.').commit()
+ self.assertTrue(ret == 1)
+ exp = 'Please resolve all conflicts before committing using "osc resolved FILE"!\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testConflictfile_filesremote')
+ self._check_conflictlist('merge\n')
+
+ @GET('http://localhost/source/osctest/nochanges?rev=latest', file='testNoChanges_filesremote')
+ @POST('http://localhost/source/osctest/nochanges?cmd=getprojectservices',
+ exp='', text='<services />')
+ def test_nochanges(self):
+ """package has no changes (which can be committed)"""
+ self._change_to_pkg('nochanges')
+ p = osc.core.Package('.')
+ ret = p.commit()
+ self.assertTrue(ret == 1)
+ exp = 'nothing to do for package nochanges\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_status(p, 'foo', 'S')
+ self._check_status(p, 'merge', '!')
+ self._check_status(p, 'nochange', ' ')
+
+ @GET('http://localhost/source/osctest/multiple?rev=latest', file='testMultiple_filesremote')
+ @POST('http://localhost/source/osctest/multiple?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/multiple?comment=&cmd=commitfilelist&user=Admin',
+ file='testMultiple_missingfilelist', expfile='testMultiple_lfilelist')
+ @PUT('http://localhost/source/osctest/multiple/nochange?rev=repository', exp='This file did change.\n', text=rev_dummy)
+ @PUT('http://localhost/source/osctest/multiple/add?rev=repository', exp='added file\n', text=rev_dummy)
+ @PUT('http://localhost/source/osctest/multiple/add2?rev=repository', exp='add2\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/multiple?comment=&cmd=commitfilelist&user=Admin',
+ file='testMultiple_cfilesremote', expfile='testMultiple_lfilelist')
+ def test_multiple(self):
+ """a simple commit (only one modified file)"""
+ self._change_to_pkg('multiple')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Deleting foo\nDeleting merge\nSending nochange\n' \
+ 'Sending add\nSending add2\nTransmitting file data ...\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testMultiple_cfilesremote')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'merge')))
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'foo')
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'merge')
+ self._check_status(p, 'add', ' ')
+ self._check_status(p, 'add2', ' ')
+ self._check_status(p, 'nochange', ' ')
+
+ @GET('http://localhost/source/osctest/multiple?rev=latest', file='testPartial_filesremote')
+ @POST('http://localhost/source/osctest/multiple?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/multiple?comment=&cmd=commitfilelist&user=Admin',
+ file='testPartial_missingfilelist', expfile='testPartial_lfilelist')
+ @PUT('http://localhost/source/osctest/multiple/add?rev=repository', exp='added file\n', text=rev_dummy)
+ @PUT('http://localhost/source/osctest/multiple/nochange?rev=repository', exp='This file did change.\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/multiple?comment=&cmd=commitfilelist&user=Admin',
+ file='testPartial_cfilesremote', expfile='testPartial_lfilelist')
+ def test_partial(self):
+ """commit only some files"""
+ self._change_to_pkg('multiple')
+ p = osc.core.Package('.')
+ p.todo = ['foo', 'add', 'nochange']
+ p.commit()
+ exp = 'Deleting foo\nSending nochange\n' \
+ 'Sending add\nTransmitting file data ..\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testPartial_cfilesremote')
+ self._check_addlist('add2\n')
+ self._check_deletelist('merge\n')
+ self._check_status(p, 'add2', 'A')
+ self._check_status(p, 'merge', 'D')
+ self._check_status(p, 'add', ' ')
+ self._check_status(p, 'nochange', ' ')
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'foo')
+
+ @GET('http://localhost/source/osctest/simple?rev=latest', file='testSimple_filesremote')
+ @POST('http://localhost/source/osctest/simple?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/simple?comment=&cmd=commitfilelist&user=Admin',
+ file='testSimple_missingfilelist', expfile='testSimple_lfilelist')
+ @PUT('http://localhost/source/osctest/simple/nochange?rev=repository', exp='This file didn\'t change but\nis modified.\n',
+ exception=IOError('test exception'), text=rev_dummy)
+ def test_interrupt(self):
+ """interrupt a commit"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self.assertRaises(IOError, p.commit)
+ exp = 'Sending nochange\nTransmitting file data .'
+ self.assertTrue(sys.stdout.getvalue(), exp)
+ self._check_digests('testSimple_filesremote')
+ self.assertTrue(os.path.exists('nochange'))
+ self._check_status(p, 'nochange', 'M')
+
+ @GET('http://localhost/source/osctest/allstates?rev=latest', file='testPartial_filesremote')
+ @POST('http://localhost/source/osctest/allstates?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/allstates?comment=&cmd=commitfilelist&user=Admin',
+ file='testAllStates_missingfilelist', expfile='testAllStates_lfilelist')
+ @PUT('http://localhost/source/osctest/allstates/add?rev=repository', exp='added file\n', text=rev_dummy)
+ @PUT('http://localhost/source/osctest/allstates/missing?rev=repository', exp='replaced\n', text=rev_dummy)
+ @PUT('http://localhost/source/osctest/allstates/nochange?rev=repository', exp='This file did change.\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/allstates?comment=&cmd=commitfilelist&user=Admin',
+ file='testAllStates_cfilesremote', expfile='testAllStates_lfilelist')
+ def test_allstates(self):
+ """commit all files (all states are available except 'C')"""
+ self._change_to_pkg('allstates')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Deleting foo\nSending missing\nSending nochange\n' \
+ 'Sending add\nTransmitting file data ...\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testAllStates_expfiles', 'skipped')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self.assertFalse(os.path.exists('foo'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_status(p, 'add', ' ')
+ self._check_status(p, 'nochange', ' ')
+ self._check_status(p, 'merge', '!')
+ self._check_status(p, 'missing', ' ')
+ self._check_status(p, 'skipped', 'S')
+ self._check_status(p, 'test', ' ')
+
+ @GET('http://localhost/source/osctest/add?rev=latest', file='testAddfile_filesremote')
+ @POST('http://localhost/source/osctest/add?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/add?comment=&cmd=commitfilelist&user=Admin',
+ file='testAddfile_cfilesremote', expfile='testAddfile_lfilelist')
+ def test_remoteexists(self):
+ """file 'add' should be committed but already exists on the server"""
+ self._change_to_pkg('add')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Sending add\nTransmitting file data \nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testAddfile_cfilesremote')
+ self.assertTrue(os.path.exists('add'))
+ self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'add'), 'r').read())
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self._check_status(p, 'add', ' ')
+ self._check_status(p, 'foo', ' ')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'nochange', ' ')
+
+ @GET('http://localhost/source/osctest/branch?rev=latest', file='testExpand_filesremote')
+ @POST('http://localhost/source/osctest/branch?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/branch?comment=&cmd=commitfilelist&user=Admin&keeplink=1',
+ file='testExpand_missingfilelist', expfile='testExpand_lfilelist')
+ @PUT('http://localhost/source/osctest/branch/simple?rev=repository', exp='simple modified file.\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/branch?comment=&cmd=commitfilelist&user=Admin&keeplink=1',
+ file='testExpand_cfilesremote', expfile='testExpand_lfilelist')
+ @GET('http://localhost/source/osctest/branch?rev=87ea02aede261b0267aabaa97c756e7a', file='testExpand_expandedfilesremote')
+ def test_expand(self):
+ """commit an expanded package"""
+ self._change_to_pkg('branch')
+ p = osc.core.Package('.')
+ p.commit()
+ exp = 'Sending simple\nTransmitting file data .\nCommitted revision 7.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testExpand_expandedfilesremote')
+ self._check_status(p, 'simple', ' ')
+
+ @GET('http://localhost/source/osctest/added_missing?rev=latest', file='testAddedMissing_filesremote')
+ @POST('http://localhost/source/osctest/added_missing?cmd=getprojectservices',
+ exp='', text='<services />')
+ def test_added_missing(self):
+ """commit an added file which is missing"""
+ self._change_to_pkg('added_missing')
+ p = osc.core.Package('.')
+ ret = p.commit()
+ self.assertTrue(ret == 1)
+ exp = 'file \'add\' is marked as \'A\' but does not exist\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_status(p, 'add', '!')
+
+ @GET('http://localhost/source/osctest/added_missing?rev=latest', file='testAddedMissing_filesremote')
+ @POST('http://localhost/source/osctest/added_missing?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/added_missing?comment=&cmd=commitfilelist&user=Admin',
+ file='testAddedMissing_missingfilelist', expfile='testAddedMissing_lfilelist')
+ @PUT('http://localhost/source/osctest/added_missing/bar?rev=repository', exp='foobar\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/added_missing?comment=&cmd=commitfilelist&user=Admin',
+ file='testAddedMissing_cfilesremote', expfile='testAddedMissing_lfilelist')
+ def test_added_missing2(self):
+ """commit an added file, another added file missing (but it's not part of the commit)"""
+ self._change_to_pkg('added_missing')
+ p = osc.core.Package('.')
+ p.todo = ['bar']
+ p.commit()
+ exp = 'Sending bar\nTransmitting file data .\nCommitted revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_status(p, 'add', '!')
+ self._check_status(p, 'bar', ' ')
+
+ @GET('http://localhost/source/osctest/simple?rev=latest', file='testSimple_filesremote')
+ @POST('http://localhost/source/osctest/simple?cmd=getprojectservices',
+ exp='', text='<services />')
+ @POST('http://localhost/source/osctest/simple?comment=&cmd=commitfilelist&user=Admin',
+ file='testSimple_missingfilelist', expfile='testSimple_lfilelist')
+ @PUT('http://localhost/source/osctest/simple/nochange?rev=repository',
+ exp='This file didn\'t change but\nis modified.\n', text=rev_dummy)
+ @POST('http://localhost/source/osctest/simple?comment=&cmd=commitfilelist&user=Admin',
+ expfile='testSimple_lfilelist', text='an error occured', code=500)
+ def test_commitfilelist_error(self):
+ """commit modified file but when committing the filelist the server returns status 500 (see ticket #65)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self._check_status(p, 'nochange', 'M')
+ self.assertRaises(HTTPError, p.commit)
+ exp = 'Sending nochange\nTransmitting file data .'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_status(p, 'nochange', 'M')
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_conf.py b/tests/test_conf.py
new file mode 100644
index 0000000..86fb3e3
--- /dev/null
+++ b/tests/test_conf.py
@@ -0,0 +1,32 @@
+from osc.conf import passx_encode, passx_decode
+from common import OscTestCase
+
+import os
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'conf_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestConf)
+
+class TestConf(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def setUp(self):
+ return super(TestConf, self).setUp(copytree=False)
+
+ def testPassxEncodeDecode(self):
+
+ passwd = "J0e'sPassword!@#"
+ passx = passx_encode(passwd)
+ #base64.b64encode(passwd.encode('bz2'))
+ passx27 = "QlpoOTFBWSZTWaDg4dQAAAKfgCiAQABAEEAAJgCYgCAAMQAACEyYmTyei67AsYSDSaLuSKcKEhQcHDqA"
+
+ self.assertEqual(passwd, passx_decode(passx))
+ self.assertEqual(passwd, passx_decode(passx27))
+ self.assertEqual(passx, passx27)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_deletefiles.py b/tests/test_deletefiles.py
new file mode 100644
index 0000000..8b38c36
--- /dev/null
+++ b/tests/test_deletefiles.py
@@ -0,0 +1,207 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'deletefile_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestDeleteFiles)
+
+class TestDeleteFiles(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def testSimpleRemove(self):
+ """delete a file ('foo') from the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('foo')
+ self.__check_ret(ret, True, ' ')
+ self.assertFalse(os.path.exists('foo'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+
+ def testDeleteModified(self):
+ """delete modified file ('nochange') from the wc (without force)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('nochange')
+ self.__check_ret(ret, False, 'M')
+ self.assertTrue(os.path.exists('nochange'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'nochange')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_status(p, 'nochange', 'M')
+
+ def testDeleteUnversioned(self):
+ """delete an unversioned file ('toadd2') from the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('toadd2')
+ self.__check_ret(ret, False, '?')
+ self.assertTrue(os.path.exists('toadd2'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_status(p, 'toadd2', '?')
+
+ def testDeleteAdded(self):
+ """delete an added file ('toadd1') from the wc (without force)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('toadd1')
+ self.__check_ret(ret, False, 'A')
+ self.assertTrue(os.path.exists('toadd1'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_status(p, 'toadd1', 'A')
+
+ def testDeleteReplaced(self):
+ """delete an added file ('merge') from the wc (without force)"""
+ self._change_to_pkg('replace')
+ p = osc.core.Package('.')
+ ret = p.delete_file('merge')
+ self.__check_ret(ret, False, 'R')
+ self.assertTrue(os.path.exists('merge'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_addlist('toadd1\nmerge\n')
+ self._check_status(p, 'merge', 'R')
+
+ def testDeleteConflict(self):
+ """delete a file ('foo', state='C') from the wc (without force)"""
+ self._change_to_pkg('conflict')
+ p = osc.core.Package('.')
+ ret = p.delete_file('foo')
+ self.__check_ret(ret, False, 'C')
+ self.assertTrue(os.path.exists('foo'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self._check_conflictlist('foo\n')
+ self._check_status(p, 'foo', 'C')
+
+ def testDeleteModifiedForce(self):
+ """force deletion modified file ('nochange') from wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('nochange', force=True)
+ self.__check_ret(ret, True, 'M')
+ self.assertFalse(os.path.exists('nochange'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'nochange')))
+ self._check_deletelist('nochange\n')
+ self._check_status(p, 'nochange', 'D')
+
+ def testDeleteUnversionedForce(self):
+ """delete an unversioned file ('toadd2') from the wc (with force)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('toadd2', force=True)
+ self.__check_ret(ret, True, '?')
+ self.assertFalse(os.path.exists('toadd2'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'toadd2')
+
+ def testDeleteAddedForce(self):
+ """delete an added file ('toadd1') from the wc (with force)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('toadd1', force=True)
+ self.__check_ret(ret, True, 'A')
+ self.assertFalse(os.path.exists('toadd1'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'toadd1')
+
+ def testDeleteReplacedForce(self):
+ """delete an added file ('merge') from the wc (with force)"""
+ self._change_to_pkg('replace')
+ p = osc.core.Package('.')
+ ret = p.delete_file('merge', force=True)
+ self.__check_ret(ret, True, 'R')
+ self.assertFalse(os.path.exists('merge'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'merge')))
+ self._check_deletelist('merge\n')
+ self._check_addlist('toadd1\n')
+ self._check_status(p, 'merge', 'D')
+
+ def testDeleteConflictForce(self):
+ """delete a file ('foo', state='C') from the wc (with force)"""
+ self._change_to_pkg('conflict')
+ p = osc.core.Package('.')
+ ret = p.delete_file('foo', force=True)
+ self.__check_ret(ret, True, 'C')
+ self.assertFalse(os.path.exists('foo'))
+ self.assertTrue(os.path.exists('foo.r2'))
+ self.assertTrue(os.path.exists('foo.mine'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_deletelist('foo\n')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_in_conflict')))
+ self._check_status(p, 'foo', 'D')
+
+ def testDeleteMultiple(self):
+ """delete mutliple files from the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('foo')
+ self.__check_ret(ret, True, ' ')
+ ret = p.delete_file('merge')
+ self.__check_ret(ret, True, ' ')
+ self.assertFalse(os.path.exists('foo'))
+ self.assertFalse(os.path.exists('merge'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'merge')))
+ self._check_deletelist('foo\nmerge\n')
+
+ def testDeleteAlreadyDeleted(self):
+ """delete already deleted file from the wc"""
+ self._change_to_pkg('already_deleted')
+ p = osc.core.Package('.')
+ ret = p.delete_file('foo')
+ self.__check_ret(ret, True, 'D')
+ self.assertFalse(os.path.exists('foo'))
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+
+ def testDeleteAddedMissing(self):
+ """
+ delete a file which was added to the wc and is removed again
+ (via a non osc command). It's current state is '!'
+ """
+ self._change_to_pkg('delete')
+ p = osc.core.Package('.')
+ ret = p.delete_file('toadd1')
+ self.__check_ret(ret, True, '!')
+ self.assertFalse(os.path.exists('toadd1'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1')))
+ self._check_deletelist('foo\n')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added')))
+
+ def testDeleteSkippedLocalNotExistent(self):
+ """
+ delete a skipped file: no local file with that name exists
+ """
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('skipped')
+ self.__check_ret(ret, False, 'S')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+
+ def testDeleteSkippedLocalExistent(self):
+ """
+ delete a skipped file: a local file with that name exists and will be deleted
+ (for instance _service:* files have status 'S' but a local files might exist)
+ """
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ ret = p.delete_file('skipped_exists')
+ self.__check_ret(ret, True, 'S')
+ self.assertFalse(os.path.exists('skipped_exists'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted')))
+
+ def __check_ret(self, ret, exp1, exp2):
+ self.assertTrue(len(ret) == 2)
+ self.assertTrue(ret[0] == exp1)
+ self.assertTrue(ret[1] == exp2)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_difffiles.py b/tests/test_difffiles.py
new file mode 100644
index 0000000..43c8afe
--- /dev/null
+++ b/tests/test_difffiles.py
@@ -0,0 +1,336 @@
+import osc.core
+import osc.oscerr
+import os
+import re
+from common import GET, OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'difffile_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestDiffFiles)
+
+class TestDiffFiles(OscTestCase):
+ diff_hdr = 'Index: %s\n==================================================================='
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def testDiffUnmodified(self):
+ """diff an unmodified file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['merge']
+ self.__check_diff(p, '', None)
+
+ def testDiffAdded(self):
+ """diff an added file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['toadd1']
+ exp = """%s
+--- toadd1\t(revision 0)
++++ toadd1\t(revision 0)
+@@ -0,0 +1,1 @@
++toadd1
+""" % (TestDiffFiles.diff_hdr % 'toadd1')
+ self.__check_diff(p, exp, None)
+
+ def testDiffRemoved(self):
+ """diff a removed file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['somefile']
+ exp = """%s
+--- somefile\t(revision 2)
++++ somefile\t(working copy)
+@@ -1,1 +0,0 @@
+-some content
+""" % (TestDiffFiles.diff_hdr % 'somefile')
+ self.__check_diff(p, exp, None)
+
+ def testDiffMissing(self):
+ """diff a missing file (missing files are ignored)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['missing']
+ self.__check_diff(p, '', None)
+
+ def testDiffReplaced(self):
+ """diff a replaced file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['replaced']
+ exp = """%s
+--- replaced\t(revision 2)
++++ replaced\t(working copy)
+@@ -1,1 +1,1 @@
+-yet another file
++foo replaced
+""" % (TestDiffFiles.diff_hdr % 'replaced')
+ self.__check_diff(p, exp, None)
+
+ def testDiffSkipped(self):
+ """diff a skipped file (skipped files are ignored)"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['skipped']
+ self.__check_diff(p, '', None)
+
+ def testDiffConflict(self):
+ """diff a file which is in the conflict state"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['foo']
+ exp = """%s
+--- foo\t(revision 2)
++++ foo\t(working copy)
+@@ -1,1 +1,5 @@
++<<<<<<< foo.mine
++This is no test.
++=======
+ This is a simple test.
++>>>>>>> foo.r2
+""" % (TestDiffFiles.diff_hdr % 'foo')
+ self.__check_diff(p, exp, None)
+
+ def testDiffModified(self):
+ """diff a modified file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['nochange']
+ exp = """%s
+--- nochange\t(revision 2)
++++ nochange\t(working copy)
+@@ -1,1 +1,2 @@
+-This file didn't change.
++This file didn't change but
++is modified.
+""" % (TestDiffFiles.diff_hdr % 'nochange')
+ self.__check_diff(p, exp, None)
+
+ def testDiffUnversioned(self):
+ """diff an unversioned file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['toadd2']
+ self.assertRaises(osc.oscerr.OscIOError, self.__check_diff, p, '', None)
+
+ def testDiffAddedMissing(self):
+ """diff a file which has satus 'A' but the local file does not exist"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['addedmissing']
+ self.assertRaises(osc.oscerr.OscIOError, self.__check_diff, p, '', None)
+
+ def testDiffMultipleFiles(self):
+ """diff multiple files"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['nochange', 'somefile']
+ exp = """%s
+--- nochange\t(revision 2)
++++ nochange\t(working copy)
+@@ -1,1 +1,2 @@
+-This file didn't change.
++This file didn't change but
++is modified.
+%s
+--- somefile\t(revision 2)
++++ somefile\t(working copy)
+@@ -1,1 +0,0 @@
+-some content
+""" % (TestDiffFiles.diff_hdr % 'nochange', TestDiffFiles.diff_hdr % 'somefile')
+ self.__check_diff(p, exp, None)
+
+ def testDiffReplacedEmptyTodo(self):
+ """diff a complete package"""
+ self._change_to_pkg('replaced')
+ p = osc.core.Package('.')
+ exp = """%s
+--- replaced\t(revision 2)
++++ replaced\t(working copy)
+@@ -1,1 +1,1 @@
+-yet another file
++foo replaced
+""" % (TestDiffFiles.diff_hdr % 'replaced')
+ self.__check_diff(p, exp, None)
+
+ def testDiffBinaryAdded(self):
+ """diff an added binary file"""
+ self._change_to_pkg('binary')
+ p = osc.core.Package('.')
+ p.todo = ['binary_added']
+ exp = """%s
+Binary file 'binary_added' added.
+""" % (TestDiffFiles.diff_hdr % 'binary_added')
+ self.__check_diff(p, exp, None)
+
+ def testDiffBinaryDeleted(self):
+ """diff a deleted binary file"""
+ self._change_to_pkg('binary')
+ p = osc.core.Package('.')
+ p.todo = ['binary_deleted']
+ exp = """%s
+Binary file 'binary_deleted' deleted.
+""" % (TestDiffFiles.diff_hdr % 'binary_deleted')
+ self.__check_diff(p, exp, None)
+
+ def testDiffBinaryModified(self):
+ """diff a modified binary file"""
+ self._change_to_pkg('binary')
+ p = osc.core.Package('.')
+ p.todo = ['binary']
+ exp = """%s
+Binary file 'binary' has changed.
+""" % (TestDiffFiles.diff_hdr % 'binary')
+ self.__check_diff(p, exp, None)
+
+ # diff with revision
+ @GET('http://localhost/source/osctest/remote_simple_noadd?rev=3', file='testDiffRemoteNoChange_files')
+ def testDiffRemoteNoChange(self):
+ """diff against remote revision where no file changed"""
+ self._change_to_pkg('remote_simple_noadd')
+ p = osc.core.Package('.')
+ self.__check_diff(p, '', 3)
+
+ @GET('http://localhost/source/osctest/remote_simple?rev=3', file='testDiffRemoteModified_files')
+ @GET('http://localhost/source/osctest/remote_simple/merge?rev=3', file='testDiffRemoteModified_merge')
+ def testDiffRemoteModified(self):
+ """diff against a remote revision with one modified file"""
+ self._change_to_pkg('remote_simple')
+ p = osc.core.Package('.')
+ exp = """%s
+--- merge\t(revision 3)
++++ merge\t(working copy)
+@@ -1,3 +1,4 @@
+ Is it
+ possible to
+ merge this file?
++I hope so...
+%s
+--- toadd1\t(revision 0)
++++ toadd1\t(revision 0)
+@@ -0,0 +1,1 @@
++toadd1
+""" % (TestDiffFiles.diff_hdr % 'merge', TestDiffFiles.diff_hdr % 'toadd1')
+ self.__check_diff(p, exp, 3)
+
+ @GET('http://localhost/source/osctest/remote_simple?rev=3', file='testDiffRemoteDeletedLocalAdded_files')
+ def testDiffRemoteNotExistingLocalAdded(self):
+ """
+ a file which doesn't exist in a remote revision and
+ has status A in the wc
+ """
+ self._change_to_pkg('remote_simple')
+ p = osc.core.Package('.')
+ exp = """%s
+--- toadd1\t(revision 0)
++++ toadd1\t(revision 0)
+@@ -0,0 +1,1 @@
++toadd1
+""" % (TestDiffFiles.diff_hdr % 'toadd1')
+ self.__check_diff(p, exp, 3)
+
+ @GET('http://localhost/source/osctest/remote_simple_noadd?rev=3', file='testDiffRemoteExistingLocalNotExisting_files')
+ @GET('http://localhost/source/osctest/remote_simple_noadd/foobar?rev=3', file='testDiffRemoteExistingLocalNotExisting_foobar')
+ @GET('http://localhost/source/osctest/remote_simple_noadd/binary?rev=3', file='testDiffRemoteExistingLocalNotExisting_binary')
+ def testDiffRemoteExistingLocalNotExisting(self):
+ """
+ a file doesn't exist in the local wc but exists
+ in the remote revision
+ """
+ self._change_to_pkg('remote_simple_noadd')
+ p = osc.core.Package('.')
+ exp = """%s
+--- foobar\t(revision 3)
++++ foobar\t(working copy)
+@@ -1,2 +0,0 @@
+-foobar
+-barfoo
+%s
+Binary file 'binary' deleted.
+""" % (TestDiffFiles.diff_hdr % 'foobar', TestDiffFiles.diff_hdr % 'binary')
+ self.__check_diff(p, exp, 3)
+
+ @GET('http://localhost/source/osctest/remote_localmodified?rev=3', file='testDiffRemoteUnchangedLocalModified_files')
+ @GET('http://localhost/source/osctest/remote_localmodified/nochange?rev=3', file='testDiffRemoteUnchangedLocalModified_nochange')
+ @GET('http://localhost/source/osctest/remote_localmodified/binary?rev=3', file='testDiffRemoteUnchangedLocalModified_binary')
+ def testDiffRemoteUnchangedLocalModified(self):
+ """remote revision didn't change, local file is modified"""
+ self._change_to_pkg('remote_localmodified')
+ p = osc.core.Package('.')
+ exp = """%s
+--- nochange\t(revision 3)
++++ nochange\t(working copy)
+@@ -1,1 +1,2 @@
+ This file didn't change.
++oh it does
+%s
+Binary file 'binary' has changed.
+""" % (TestDiffFiles.diff_hdr % 'nochange', TestDiffFiles.diff_hdr % 'binary')
+ self.__check_diff(p, exp, 3)
+
+ @GET('http://localhost/source/osctest/remote_simple_noadd?rev=3', file='testDiffRemoteMissingLocalExisting_files')
+ def testDiffRemoteMissingLocalExisting(self):
+ """
+ remote revision misses a file which exists in the local wc (state ' ')"""
+ self._change_to_pkg('remote_simple_noadd')
+ p = osc.core.Package('.')
+ exp = """%s
+--- foo\t(revision 0)
++++ foo\t(working copy)
+@@ -0,0 +1,1 @@
++This is a simple test.
+""" % (TestDiffFiles.diff_hdr % 'foo')
+ self.__check_diff(p, exp, 3)
+
+ @GET('http://localhost/source/osctest/remote_localdelete?rev=3', file='testDiffRemoteMissingLocalDeleted_files')
+ def testDiffRemoteMissingLocalDeleted(self):
+ """
+ remote revision misses a file which is marked for
+ deletion in the local wc
+ """
+ # empty diff is expected (svn does the same)
+ self._change_to_pkg('remote_localdelete')
+ p = osc.core.Package('.')
+ self.__check_diff(p, '', 3)
+
+ def __check_diff(self, p, exp, revision=None):
+ got = ''
+ for i in p.get_diff(revision):
+ got += ''.join(i)
+
+ # When a hunk header refers to a single line in the "from"
+ # file and/or the "to" file, e.g.
+ #
+ # @@ -37,37 +41,43 @@
+ # @@ -37,39 +41,41 @@
+ # @@ -37,37 +41,41 @@
+ #
+ # some systems will avoid repeating the line number:
+ #
+ # @@ -37 +41,43 @@
+ # @@ -37,39 +41 @@
+ # @@ -37 +41 @@
+ #
+ # so we need to canonise the output to avoid false negative
+ # test failures.
+
+ # TODO: Package.get_diff should return a consistent format
+ # (regardless of the used python version)
+ def __canonise_diff(diff):
+ # we cannot use re.M because python 2.6's re.sub does
+ # not support a flags argument
+ diff = [re.sub('^@@ -(\d+) ', '@@ -\\1,\\1 ', line)
+ for line in diff.split('\n')]
+ diff = [re.sub('^(@@ -\d+,\d+) \+(\d+) ', '\\1 +\\2,\\2 ', line)
+ for line in diff]
+ return '\n'.join(diff)
+
+ got = __canonise_diff(got)
+ exp = __canonise_diff(exp)
+ self.assertEqualMultiline(got, exp)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_init_package.py b/tests/test_init_package.py
new file mode 100644
index 0000000..7347506
--- /dev/null
+++ b/tests/test_init_package.py
@@ -0,0 +1,88 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+FIXTURES_DIR = os.path.join(os.getcwd(), 'init_package_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestInitPackage)
+
+class TestInitPackage(OscTestCase):
+ def _get_fixtures_dir(self):
+ # workaround for git because it doesn't allow empty dirs
+ if not os.path.exists(os.path.join(FIXTURES_DIR, 'osctest')):
+ os.mkdir(os.path.join(FIXTURES_DIR, 'osctest'))
+ return FIXTURES_DIR
+
+ def tearDown(self):
+ if os.path.exists(os.path.join(FIXTURES_DIR, 'osctest')):
+ os.rmdir(os.path.join(FIXTURES_DIR, 'osctest'))
+ OscTestCase.tearDown(self)
+
+ def test_simple(self):
+ """initialize a package dir"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ osc.core.Package.init_package('http://localhost', 'osctest', 'testpkg', pac_dir)
+ storedir = os.path.join(pac_dir, osc.core.store)
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_meta_mode')))
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_size_limit')))
+ self._check_list(os.path.join(storedir, '_project'), 'osctest\n')
+ self._check_list(os.path.join(storedir, '_package'), 'testpkg\n')
+ self._check_list(os.path.join(storedir, '_files'), '<directory />\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+
+ def test_size_limit(self):
+ """initialize a package dir with size_limit parameter"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ osc.core.Package.init_package('http://localhost', 'osctest', 'testpkg', pac_dir, size_limit=42)
+ storedir = os.path.join(pac_dir, osc.core.store)
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_meta_mode')))
+ self._check_list(os.path.join(storedir, '_size_limit'), '42\n')
+ self._check_list(os.path.join(storedir, '_project'), 'osctest\n')
+ self._check_list(os.path.join(storedir, '_package'), 'testpkg\n')
+ self._check_list(os.path.join(storedir, '_files'), '<directory />\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+
+ def test_meta_mode(self):
+ """initialize a package dir with meta paramter"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ osc.core.Package.init_package('http://localhost', 'osctest', 'testpkg', pac_dir, meta=True)
+ storedir = os.path.join(pac_dir, osc.core.store)
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_size_limit')))
+ self._check_list(os.path.join(storedir, '_meta_mode'), '')
+ self._check_list(os.path.join(storedir, '_project'), 'osctest\n')
+ self._check_list(os.path.join(storedir, '_package'), 'testpkg\n')
+ self._check_list(os.path.join(storedir, '_files'), '<directory />\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+
+ def test_dirExists(self):
+ """initialize a package dir (dir already exists)"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ os.mkdir(pac_dir)
+ osc.core.Package.init_package('http://localhost', 'osctest', 'testpkg', pac_dir)
+ storedir = os.path.join(pac_dir, osc.core.store)
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_meta_mode')))
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_size_limit')))
+ self._check_list(os.path.join(storedir, '_project'), 'osctest\n')
+ self._check_list(os.path.join(storedir, '_package'), 'testpkg\n')
+ self._check_list(os.path.join(storedir, '_files'), '<directory />\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+
+ def test_storedirExists(self):
+ """initialize a package dir (dir+storedir already exists)"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ os.mkdir(pac_dir)
+ os.mkdir(os.path.join(pac_dir, osc.core.store))
+ self.assertRaises(osc.oscerr.OscIOError, osc.core.Package.init_package, 'http://localhost', 'osctest', 'testpkg', pac_dir)
+
+ def test_dirIsFile(self):
+ """initialize a package dir (dir is a file)"""
+ pac_dir = os.path.join(self.tmpdir, 'testpkg')
+ os.mkdir(pac_dir)
+ open(os.path.join(pac_dir, osc.core.store), 'w').write('foo\n')
+ self.assertRaises(osc.oscerr.OscIOError, osc.core.Package.init_package, 'http://localhost', 'osctest', 'testpkg', pac_dir)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_init_project.py b/tests/test_init_project.py
new file mode 100644
index 0000000..f4714dc
--- /dev/null
+++ b/tests/test_init_project.py
@@ -0,0 +1,71 @@
+import osc.core
+import osc.oscerr
+import os
+from common import GET, OscTestCase
+FIXTURES_DIR = os.path.join(os.getcwd(), 'init_project_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestInitProject)
+
+class TestInitProject(OscTestCase):
+ def _get_fixtures_dir(self):
+ # workaround for git because it doesn't allow empty dirs
+ if not os.path.exists(os.path.join(FIXTURES_DIR, 'osctest')):
+ os.mkdir(os.path.join(FIXTURES_DIR, 'osctest'))
+ return FIXTURES_DIR
+
+ def tearDown(self):
+ if os.path.exists(os.path.join(FIXTURES_DIR, 'osctest')):
+ os.rmdir(os.path.join(FIXTURES_DIR, 'osctest'))
+ OscTestCase.tearDown(self)
+
+ def test_simple(self):
+ """initialize a project dir"""
+ prj_dir = os.path.join(self.tmpdir, 'testprj')
+ prj = osc.core.Project.init_project('http://localhost', prj_dir, 'testprj', getPackageList=False)
+ self.assertTrue(isinstance(prj, osc.core.Project))
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self._check_list(os.path.join(storedir, '_project'), 'testprj\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+ self._check_list(os.path.join(storedir, '_packages'), '<project name="testprj" />')
+
+ def test_dirExists(self):
+ """initialize a project dir but the dir already exists"""
+ prj_dir = os.path.join(self.tmpdir, 'testprj')
+ os.mkdir(prj_dir)
+ prj = osc.core.Project.init_project('http://localhost', prj_dir, 'testprj', getPackageList=False)
+ self.assertTrue(isinstance(prj, osc.core.Project))
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self._check_list(os.path.join(storedir, '_project'), 'testprj\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+ self._check_list(os.path.join(storedir, '_packages'), '<project name="testprj" />')
+
+ def test_storedirExists(self):
+ """initialize a project dir but the storedir already exists"""
+ prj_dir = os.path.join(self.tmpdir, 'testprj')
+ os.mkdir(prj_dir)
+ os.mkdir(os.path.join(prj_dir, osc.core.store))
+ self.assertRaises(osc.oscerr.OscIOError, osc.core.Project.init_project, 'http://localhost', prj_dir, 'testprj')
+
+ @GET('http://localhost/source/testprj', text='<directory count="0" />')
+ def test_no_package_tracking(self):
+ """initialize a project dir but disable package tracking; enable getPackageList=True;
+ disable wc_check (because we didn't disable the package tracking before the Project class
+ was imported therefore REQ_STOREFILES contains '_packages')
+ """
+ import osc.conf
+ # disable package tracking
+ osc.conf.config['do_package_tracking'] = False
+ prj_dir = os.path.join(self.tmpdir, 'testprj')
+ os.mkdir(prj_dir)
+ prj = osc.core.Project.init_project('http://localhost', prj_dir, 'testprj', False, wc_check=False)
+ self.assertTrue(isinstance(prj, osc.core.Project))
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self._check_list(os.path.join(storedir, '_project'), 'testprj\n')
+ self._check_list(os.path.join(storedir, '_apiurl'), 'http://localhost\n')
+ self.assertFalse(os.path.exists(os.path.join(storedir, '_packages')))
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_package_status.py b/tests/test_package_status.py
new file mode 100644
index 0000000..430adec
--- /dev/null
+++ b/tests/test_package_status.py
@@ -0,0 +1,86 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'project_package_status_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestPackageStatus)
+
+class TestPackageStatus(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def test_allfiles(self):
+ """get the status of all files in the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ exp_st = [('A', 'add'), ('?', 'exists'), ('D', 'foo'), ('!', 'merge'), ('R', 'missing'),
+ ('!', 'missing_added'), ('M', 'nochange'), ('S', 'skipped'), (' ', 'test')]
+ st = p.get_status()
+ self.assertEqual(exp_st, st)
+
+ def test_todo(self):
+ """
+ get the status of some files in the wc.
+ """
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['test', 'missing_added', 'foo']
+ exp_st = [('D', 'foo'), ('!', 'missing_added')]
+ st = p.get_status(False, ' ')
+ self.assertEqual(exp_st, st)
+
+ def test_todo_noexcl(self):
+ """ get the status of some files in the wc. """
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['test', 'missing_added', 'foo']
+ exp_st = [('D', 'foo'), ('!', 'missing_added'), (' ', 'test')]
+ st = p.get_status()
+ self.assertEqual(exp_st, st)
+
+ def test_exclude_state(self):
+ """get the status of all files in the wc but exclude some states"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ exp_st = [('A', 'add'), ('?', 'exists'), ('D', 'foo')]
+ st = p.get_status(False, '!', 'S', ' ', 'M', 'R')
+ self.assertEqual(exp_st, st)
+
+ def test_nonexistent(self):
+ """get the status of a non existent file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.todo = ['doesnotexist']
+ self.assertRaises(osc.oscerr.OscIOError, p.get_status)
+
+ def test_conflict(self):
+ """get status of the wc (one file in conflict state)"""
+ self._change_to_pkg('conflict')
+ p = osc.core.Package('.')
+ exp_st = [('C', 'conflict'), ('?', 'exists'), (' ', 'test')]
+ st = p.get_status()
+ self.assertEqual(exp_st, st)
+
+ def test_excluded(self):
+ """get status of the wc (ignore excluded files); package has state ' '"""
+ self._change_to_pkg('excluded')
+ p = osc.core.Package('.')
+ exp_st = [('?', 'exists'), ('M', 'modified')]
+ st = p.get_status(False, ' ')
+ self.assertEqual(exp_st, st)
+
+ def test_noexcluded(self):
+ """get status of the wc (include excluded files)"""
+ self._change_to_pkg('excluded')
+ p = osc.core.Package('.')
+ exp_st = [('?', '_linkerror'), ('?', 'exists'), ('?', 'foo.orig'), ('M', 'modified'), (' ', 'test')]
+ st = p.get_status(True)
+ self.assertEqual(exp_st, st)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_prdiff.py b/tests/test_prdiff.py
new file mode 100644
index 0000000..cff6327
--- /dev/null
+++ b/tests/test_prdiff.py
@@ -0,0 +1,271 @@
+import osc.commandline
+import osc.core
+import osc.oscerr
+import os
+import re
+import sys
+from common import GET, POST, OscTestCase, addExpectedRequest, EXPECTED_REQUESTS
+
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'prdiff_fixtures')
+API_URL = 'http://localhost/'
+UPSTREAM = 'some:project'
+BRANCH = 'home:user:branches:' + UPSTREAM
+
+def rdiff_url(pkg, oldprj, newprj):
+ return API_URL + 'source/%s/%s?unified=1&opackage=%s&oproject=%s&cmd=diff&expand=1&tarlimit=0&filelimit=0' % \
+ (newprj, pkg, pkg, oldprj.replace(':', '%3A'))
+
+def request_url(prj):
+ return API_URL + 'search/request?match=%%28state%%2F%%40name%%3D%%27new%%27+or+state%%2F%%40name%%3D%%27review%%27%%29+and+%%28action%%2Ftarget%%2F%%40project%%3D%%27%s%%27+or+submit%%2Ftarget%%2F%%40project%%3D%%27%s%%27+or+action%%2Fsource%%2F%%40project%%3D%%27%s%%27+or+submit%%2Fsource%%2F%%40project%%3D%%27%s%%27%%29' % \
+ tuple([prj.replace(':', '%3A')] * 4)
+
+def GET_PROJECT_PACKAGES(*projects):
+ def decorator(test_method):
+ def wrapped_test_method(*args):
+ for project in projects:
+ addExpectedRequest('GET', API_URL + 'source/' + project,
+ file='%s/directory' % project)
+ test_method(*args)
+ # "rename" method otherwise we cannot specify a TestCaseClass.testName
+ # cmdline arg when using unittest.main()
+ wrapped_test_method.__name__ = test_method.__name__
+ return wrapped_test_method
+ return decorator
+
+def POST_RDIFF(oldprj, newprj):
+ def decorator(test_method):
+ def wrapped_test_method(*args):
+ addExpectedRequest('POST', rdiff_url('common-one', oldprj, newprj), exp='', text='')
+ addExpectedRequest('POST', rdiff_url('common-two', oldprj, newprj), exp='', file='common-two-diff')
+ addExpectedRequest('POST', rdiff_url('common-three', oldprj, newprj), exp='', text='')
+ test_method(*args)
+ # "rename" method otherwise we cannot specify a TestCaseClass.testName
+ # cmdline arg when using unittest.main()
+ wrapped_test_method.__name__ = test_method.__name__
+ return wrapped_test_method
+ return decorator
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestProjectDiff)
+
+class TestProjectDiff(OscTestCase):
+ diff_hdr = 'Index: %s\n==================================================================='
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def _change_to_tmpdir(self, *args):
+ os.chdir(os.path.join(self.tmpdir, *args))
+
+ def _run_prdiff(self, *args):
+ """Runs osc prdiff, returning captured STDOUT as a string."""
+ cli = osc.commandline.Osc()
+ argv = ['osc', '--no-keyring', '--no-gnome-keyring', 'prdiff']
+ argv.extend(args)
+ cli.main(argv=argv)
+ return sys.stdout.getvalue()
+
+
+ def testPrdiffTooManyArgs(self):
+ def runner():
+ self._run_prdiff('one', 'two', 'superfluous-arg')
+ self.assertRaises(osc.oscerr.WrongArgs, runner)
+
+
+ @GET_PROJECT_PACKAGES(UPSTREAM, BRANCH)
+ @POST_RDIFF(UPSTREAM, BRANCH)
+ @POST(rdiff_url('only-in-new', UPSTREAM, BRANCH), exp='', text='')
+ def testPrdiffZeroArgs(self):
+ exp = """identical: common-one
+differs: common-two
+identical: common-three
+identical: only-in-new
+"""
+ def runner():
+ self._run_prdiff()
+
+ os.chdir('/tmp')
+ self.assertRaises(osc.oscerr.WrongArgs, runner)
+
+ self._change_to_tmpdir(FIXTURES_DIR, UPSTREAM)
+ self.assertRaises(osc.oscerr.WrongArgs, runner)
+
+ self._change_to_tmpdir(FIXTURES_DIR, BRANCH)
+ out = self._run_prdiff()
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES(UPSTREAM, BRANCH)
+ @POST_RDIFF(UPSTREAM, BRANCH)
+ @POST(rdiff_url('only-in-new', UPSTREAM, BRANCH), exp='', text='')
+ def testPrdiffOneArg(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+identical: common-three
+identical: only-in-new
+"""
+ out = self._run_prdiff('home:user:branches:some:project')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST_RDIFF('old:prj', 'new:prj')
+ def testPrdiffTwoArgs(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+identical: common-three
+"""
+ out = self._run_prdiff('old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST_RDIFF('old:prj', 'new:prj')
+ def testPrdiffOldOnly(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+identical: common-three
+old only: only-in-old
+"""
+ out = self._run_prdiff('--show-not-in-new', 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST_RDIFF('old:prj', 'new:prj')
+ def testPrdiffNewOnly(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+identical: common-three
+new only: only-in-new
+"""
+ out = self._run_prdiff('--show-not-in-old', 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST_RDIFF('old:prj', 'new:prj')
+ def testPrdiffDiffstat(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+
+ common-two | 1 +
+ 1 file changed, 1 insertion(+)
+
+identical: common-three
+"""
+ out = self._run_prdiff('--diffstat', 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST_RDIFF('old:prj', 'new:prj')
+ def testPrdiffUnified(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+
+Index: common-two
+===================================================================
+--- common-two\t2013-01-18 19:18:38.225983117 +0000
++++ common-two\t2013-01-18 19:19:27.882082325 +0000
+@@ -1,4 +1,5 @@
+ line one
+ line two
+ line three
++an extra line
+ last line
+
+identical: common-three
+"""
+ out = self._run_prdiff('--unified', 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST(rdiff_url('common-two', 'old:prj', 'new:prj'), exp='', file='common-two-diff')
+ @POST(rdiff_url('common-three', 'old:prj', 'new:prj'), exp='', text='')
+ def testPrdiffInclude(self):
+ self._change_to_tmpdir()
+ exp = """differs: common-two
+identical: common-three
+"""
+ out = self._run_prdiff('--include', 'common-t',
+ 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST(rdiff_url('common-two', 'old:prj', 'new:prj'), exp='', file='common-two-diff')
+ @POST(rdiff_url('common-three', 'old:prj', 'new:prj'), exp='', text='')
+ def testPrdiffExclude(self):
+ self._change_to_tmpdir()
+ exp = """differs: common-two
+identical: common-three
+"""
+ out = self._run_prdiff('--exclude', 'one', 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES('old:prj', 'new:prj')
+ @POST(rdiff_url('common-two', 'old:prj', 'new:prj'), exp='', file='common-two-diff')
+ def testPrdiffIncludeExclude(self):
+ self._change_to_tmpdir()
+ exp = """differs: common-two
+"""
+ out = self._run_prdiff('--include', 'common-t',
+ '--exclude', 'three',
+ 'old:prj', 'new:prj')
+ self.assertEqualMultiline(out, exp)
+
+
+ @GET_PROJECT_PACKAGES(UPSTREAM, BRANCH)
+ @GET(request_url(UPSTREAM), exp='', file='request')
+ @POST(rdiff_url('common-one', UPSTREAM, BRANCH), exp='', text='')
+ @POST(rdiff_url('common-two', UPSTREAM, BRANCH), exp='', file='common-two-diff')
+ @POST(rdiff_url('common-three', UPSTREAM, BRANCH), exp='', file='common-two-diff')
+ @POST(rdiff_url('only-in-new', UPSTREAM, BRANCH), exp='', text='')
+ def testPrdiffRequestsMatching(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+
+148023 State:new By:user When:2013-01-11T11:04:14
+ submit: home:user:branches:some:project/common-two@7 -> some:project
+ Descr: - Fix it to work - Improve support for something
+
+differs: common-three
+identical: only-in-new
+"""
+ out = self._run_prdiff('--requests', UPSTREAM, BRANCH)
+ self.assertEqualMultiline(out, exp)
+
+
+ # Reverse the direction of the diff.
+ @GET_PROJECT_PACKAGES(BRANCH, UPSTREAM)
+ @GET(request_url(BRANCH), exp='', file='no-requests')
+ @POST(rdiff_url('common-one', BRANCH, UPSTREAM), exp='', text='')
+ @POST(rdiff_url('common-two', BRANCH, UPSTREAM), exp='', file='common-two-diff')
+ @POST(rdiff_url('common-three', BRANCH, UPSTREAM), exp='', file='common-two-diff')
+ @POST(rdiff_url('only-in-new', BRANCH, UPSTREAM), exp='', text='')
+ def testPrdiffRequestsSwitched(self):
+ self._change_to_tmpdir()
+ exp = """identical: common-one
+differs: common-two
+differs: common-three
+identical: only-in-new
+"""
+ out = self._run_prdiff('--requests', BRANCH, UPSTREAM)
+ self.assertEqualMultiline(out, exp)
+
+
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_project_status.py b/tests/test_project_status.py
new file mode 100644
index 0000000..3c8497f
--- /dev/null
+++ b/tests/test_project_status.py
@@ -0,0 +1,161 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'project_package_status_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestProjectStatus)
+
+class TestProjectStatus(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def test_simple(self):
+ """get the status of a package with state ' '"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = ' '
+ st = prj.status('simple')
+ self.assertEqual(exp_st, st)
+
+ def test_added(self):
+ """get the status of an added package"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = 'A'
+ st = prj.status('added')
+ self.assertEqual(exp_st, st)
+
+ def test_deleted(self):
+ """get the status of a deleted package"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = 'D'
+ st = prj.status('deleted')
+ self.assertEqual(exp_st, st)
+
+ def test_added_deleted(self):
+ """
+ get the status of a package which was added and deleted
+ afterwards (with a non osc command)
+ """
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = '!'
+ st = prj.status('added_deleted')
+ self.assertEqual(exp_st, st)
+
+ def test_missing(self):
+ """
+ get the status of a package with state " "
+ which was removed by a non osc command
+ """
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = '!'
+ st = prj.status('missing')
+ self.assertEqual(exp_st, st)
+
+ def test_deleted_deleted(self):
+ """
+ get the status of a package which was deleted (with an
+ osc command) and afterwards the package directory was
+ deleted with a non osc command
+ """
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = 'D'
+ st = prj.status('deleted_deleted')
+ self.assertEqual(exp_st, st)
+
+ def test_unversioned_exists(self):
+ """get the status of an unversioned package"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = '?'
+ st = prj.status('excluded')
+ self.assertEqual(exp_st, st)
+
+ def test_unversioned_nonexistent(self):
+ """get the status of an unversioned, nonexistent package"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ self.assertRaises(osc.oscerr.OscIOError, prj.status, 'doesnotexist')
+
+ def test_get_status(self):
+ """get the status of the complete project"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = [(' ', 'conflict'), (' ', 'simple'), ('A', 'added'), ('D', 'deleted'),
+ ('!', 'missing'), ('!', 'added_deleted'), ('D', 'deleted_deleted'), ('?', 'excluded')]
+ st = prj.get_status()
+ self.assertEqual(exp_st, st)
+
+ def test_get_status_excl(self):
+ """get the status of the complete project (exclude some states)"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ exp_st = [('A', 'added'), ('!', 'missing'), ('!', 'added_deleted')]
+ st = prj.get_status('D', ' ', '?')
+ self.assertEqual(exp_st, st)
+
+ def test_get_pacobj_simple(self):
+ """package exists"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('simple')
+ self.assertTrue(isinstance(p, osc.core.Package))
+ self.assertEqual(p.name, 'simple')
+
+ def test_get_pacobj_added(self):
+ """package has state 'A', also test pac_kwargs"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('added', progress_obj={})
+ self.assertTrue(isinstance(p, osc.core.Package))
+ self.assertEqual(p.name, 'added')
+ self.assertEqual(p.progress_obj, {})
+
+ def test_get_pacobj_deleted(self):
+ """package has state 'D' and exists, also test pac_args"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('deleted', {})
+ self.assertTrue(isinstance(p, osc.core.Package))
+ self.assertEqual(p.name, 'deleted')
+ self.assertEqual(p.progress_obj, {})
+
+ def test_get_pacobj_missing(self):
+ """package is missing"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('missing')
+ self.assertTrue(isinstance(p, type(None)))
+
+ def test_get_pacobj_deleted_deleted(self):
+ """package has state 'D' and does not exist"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('deleted_deleted')
+ self.assertTrue(isinstance(p, type(None)))
+
+ def test_get_pacobj_unversioned(self):
+ """package/dir has state '?'"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('excluded')
+ self.assertTrue(isinstance(p, type(None)))
+
+ def test_get_pacobj_nonexistent(self):
+ """package/dir does not exist"""
+ self._change_to_pkg('.')
+ prj = osc.core.Project('.', getPackageList=False)
+ p = prj.get_pacobj('doesnotexist')
+ self.assertTrue(isinstance(p, type(None)))
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_repairwc.py b/tests/test_repairwc.py
new file mode 100644
index 0000000..f2b090b
--- /dev/null
+++ b/tests/test_repairwc.py
@@ -0,0 +1,265 @@
+import osc.core
+import osc.oscerr
+import os
+import sys
+from common import GET, PUT, POST, DELETE, OscTestCase
+from xml.etree import cElementTree as ET
+FIXTURES_DIR = os.path.join(os.getcwd(), 'repairwc_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestRepairWC)
+
+class TestRepairWC(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def __assertNotRaises(self, exception, meth, *args, **kwargs):
+ try:
+ meth(*args, **kwargs)
+ except exception:
+ self.fail('%s raised' % exception.__name__)
+
+ def test_working_empty(self):
+ """consistent, empty working copy"""
+ self._change_to_pkg('working_empty')
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_working_nonempty(self):
+ """
+ consistent, non-empty working copy. One file is in conflict,
+ one file is marked for deletion and one file has state 'A'
+ """
+ self._change_to_pkg('working_nonempty')
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_buildfiles(self):
+ """
+ wc has a _buildconfig_prj_arch and a _buildinfo_prj_arch.xml in the storedir
+ """
+ self._change_to_pkg('buildfiles')
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ @GET('http://localhost/source/osctest/simple1/foo?rev=1', text='This is a simple test.\n')
+ def test_simple1(self):
+ """a file is marked for deletion but storefile doesn't exist"""
+ self._change_to_pkg('simple1')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple2(self):
+ """a file "somefile" exists in the storedir which isn't tracked"""
+ self._change_to_pkg('simple2')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'somefile')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple3(self):
+ """toadd1 has state 'A' and a file .osc/toadd1 exists"""
+ self._change_to_pkg('simple3')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_addlist('toadd1\n')
+ self._check_status(p, 'toadd1', 'A')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple4(self):
+ """a file is listed in _to_be_deleted but isn't present in _files"""
+ self._change_to_pkg('simple4')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple5(self):
+ """a file is listed in _in_conflict but isn't present in _files"""
+ self._change_to_pkg('simple5')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_in_conflict')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ @GET('http://localhost/source/osctest/simple6/foo?rev=1', text='This is a simple test.\n')
+ def test_simple6(self):
+ """
+ a file is listed in _to_be_deleted and is present
+ in _files but the storefile is missing
+ """
+ self._change_to_pkg('simple6')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple7(self):
+ """files marked as skipped don't exist in the storedir"""
+ self._change_to_pkg('simple7')
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_simple8(self):
+ """
+ a file is marked as skipped but the skipped file exists in the storedir
+ """
+ self._change_to_pkg('simple8')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'skipped')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'M')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'toadd1', '?')
+ self._check_status(p, 'skipped', 'S')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ @GET('http://localhost/source/osctest/multiple/merge?rev=1', text='Is it\npossible to\nmerge this file?I hope so...\n')
+ @GET('http://localhost/source/osctest/multiple/nochange?rev=1', text='This file didn\'t change.\n')
+ def test_multiple(self):
+ """
+ a storefile is missing, a file is listed in _to_be_deleted
+ but is not present in _files, a file is listed in _in_conflict
+ but the storefile is missing and a file exists in the storedir
+ but is not present in _files
+ """
+ self._change_to_pkg('multiple')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair()
+ self.assertTrue(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'unknown_file')))
+ self._check_deletelist('foo\n')
+ self._check_status(p, 'foo', 'D')
+ self._check_status(p, 'nochange', 'C')
+ self._check_status(p, 'merge', ' ')
+ self._check_status(p, 'foobar', 'A')
+ self._check_status(p, 'toadd1', '?')
+ # additional cleanup check
+ self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+
+ def test_noapiurl(self):
+ """the package wc has no _apiurl file"""
+ self._change_to_pkg('noapiurl')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair('http://localhost')
+ self.assertTrue(os.path.exists(os.path.join('.osc', '_apiurl')))
+ self.assertEqual(open(os.path.join('.osc', '_apiurl')).read(), 'http://localhost\n')
+ self.assertEqual(p.apiurl, 'http://localhost')
+
+ def test_invalidapiurl(self):
+ """the package wc has an invalid apiurl file (invalid url format)"""
+ self._change_to_pkg('invalid_apiurl')
+ p = osc.core.Package('.', wc_check=False)
+ p.wc_repair('http://localhost')
+ self.assertTrue(os.path.exists(os.path.join('.osc', '_apiurl')))
+ self.assertEqual(open(os.path.join('.osc', '_apiurl')).read(), 'http://localhost\n')
+ self.assertEqual(p.apiurl, 'http://localhost')
+
+ def test_invalidapiurl_param(self):
+ """pass an invalid apiurl to wc_repair"""
+ try:
+ from urllib.error import URLError
+ except ImportError:
+ from urllib2 import URLError
+ self._change_to_pkg('invalid_apiurl')
+ p = osc.core.Package('.', wc_check=False)
+ self.assertRaises(URLError, p.wc_repair, 'http:/localhost')
+ self.assertRaises(URLError, p.wc_repair, 'invalid')
+
+ def test_noapiurlNotExistingApiurl(self):
+ """the package wc has no _apiurl file and no apiurl is passed to repairwc"""
+ self._change_to_pkg('noapiurl')
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.')
+ p = osc.core.Package('.', wc_check=False)
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, p.wc_repair)
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_apiurl')))
+
+ def test_project_noapiurl(self):
+ """the project wc has no _apiurl file"""
+ import shutil
+ prj_dir = os.path.join(self.tmpdir, 'prj_noapiurl')
+ shutil.copytree(os.path.join(self._get_fixtures_dir(), 'prj_noapiurl'), prj_dir)
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Project, prj_dir, getPackageList=False)
+ prj = osc.core.Project(prj_dir, wc_check=False, getPackageList=False)
+ prj.wc_repair('http://localhost')
+ self.assertTrue(os.path.exists(os.path.join(storedir, '_apiurl')))
+ self.assertTrue(os.path.exists(os.path.join(storedir, '_apiurl')))
+ self.assertEqual(open(os.path.join(storedir, '_apiurl'), 'r').read(), 'http://localhost\n')
+
+ def test_project_invalidapiurl(self):
+ """the project wc has an invalid _apiurl file (invalid url format)"""
+ import shutil
+ prj_dir = os.path.join(self.tmpdir, 'prj_invalidapiurl')
+ shutil.copytree(os.path.join(self._get_fixtures_dir(), 'prj_invalidapiurl'), prj_dir)
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Project, prj_dir, getPackageList=False)
+ prj = osc.core.Project(prj_dir, wc_check=False, getPackageList=False)
+ prj.wc_repair('http://localhost')
+ self.assertTrue(os.path.exists(os.path.join(storedir, '_apiurl')))
+ self.assertTrue(os.path.exists(os.path.join(storedir, '_apiurl')))
+ self.assertEqual(open(os.path.join(storedir, '_apiurl'), 'r').read(), 'http://localhost\n')
+
+ def test_project_invalidapiurl_param(self):
+ """pass an invalid apiurl to wc_repair"""
+ import shutil
+ try:
+ from urllib.error import URLError
+ except ImportError:
+ from urllib2 import URLError
+ prj_dir = os.path.join(self.tmpdir, 'prj_invalidapiurl')
+ shutil.copytree(os.path.join(self._get_fixtures_dir(), 'prj_invalidapiurl'), prj_dir)
+ storedir = os.path.join(prj_dir, osc.core.store)
+ self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Project, prj_dir, getPackageList=False)
+ prj = osc.core.Project(prj_dir, wc_check=False, getPackageList=False)
+ self.assertRaises(URLError, prj.wc_repair, 'http:/localhost')
+ self.assertRaises(URLError, prj.wc_repair, 'invalid')
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_request.py b/tests/test_request.py
new file mode 100644
index 0000000..d7cdd5a
--- /dev/null
+++ b/tests/test_request.py
@@ -0,0 +1,577 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'request_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestRequest)
+
+class TestRequest(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def setUp(self):
+ OscTestCase.setUp(self, copytree=False)
+
+ def test_createsr(self):
+ """create a simple submitrequest"""
+ r = osc.core.Request()
+ r.add_action('submit', src_project='foo', src_package='bar', src_rev='42',
+ tgt_project='foobar', tgt_package='bar')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'foo')
+ self.assertEqual(r.actions[0].src_package, 'bar')
+ self.assertEqual(r.actions[0].src_rev, '42')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertTrue(r.actions[0].opt_sourceupdate is None)
+ self.assertTrue(r.actions[0].opt_updatelink is None)
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ self.assertRaises(AttributeError, getattr, r.actions[0], 'doesnotexist')
+ exp = """<request>
+ <action type="submit">
+ <source package="bar" project="foo" rev="42" />
+ <target package="bar" project="foobar" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_createsr_with_option(self):
+ """create a submitrequest with option"""
+ """create a simple submitrequest"""
+ r = osc.core.Request()
+ r.add_action('submit', src_project='foo', src_package='bar',
+ tgt_project='foobar', tgt_package='bar', opt_sourceupdate='cleanup', opt_updatelink='1')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'foo')
+ self.assertEqual(r.actions[0].src_package, 'bar')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertEqual(r.actions[0].opt_sourceupdate, 'cleanup')
+ self.assertEqual(r.actions[0].opt_updatelink, '1')
+ self.assertTrue(r.actions[0].src_rev is None)
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ self.assertRaises(AttributeError, getattr, r.actions[0], 'doesnotexist')
+ exp = """<request>
+ <action type="submit">
+ <source package="bar" project="foo" />
+ <target package="bar" project="foobar" />
+ <options>
+ <sourceupdate>cleanup</sourceupdate>
+ <updatelink>1</updatelink>
+ </options>
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_createsr_missing_tgt_package(self):
+ """create a submitrequest with missing target package"""
+ r = osc.core.Request()
+ r.add_action('submit', src_project='foo', src_package='bar',
+ tgt_project='foobar')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'foo')
+ self.assertEqual(r.actions[0].src_package, 'bar')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ self.assertTrue(r.actions[0].tgt_package is None)
+ self.assertRaises(AttributeError, getattr, r.actions[0], 'doesnotexist')
+ exp = """<request>
+ <action type="submit">
+ <source package="bar" project="foo" />
+ <target project="foobar" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_createsr_invalid_argument(self):
+ """create a submitrequest with invalid action argument"""
+ r = osc.core.Request()
+ self.assertRaises(osc.oscerr.WrongArgs, r.add_action, 'submit', src_project='foo', src_invalid='bar')
+
+ def test_create_request_invalid_type(self):
+ """create a request with an invalid action type"""
+ r = osc.core.Request()
+ self.assertRaises(osc.oscerr.WrongArgs, r.add_action, 'invalid', foo='bar')
+
+ def test_create_add_role_person(self):
+ """create an add_role request (person element)"""
+ r = osc.core.Request()
+ r.add_action('add_role', tgt_project='foo', tgt_package='bar', person_name='user', person_role='reader')
+ self.assertEqual(r.actions[0].type, 'add_role')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertEqual(r.actions[0].person_name, 'user')
+ self.assertEqual(r.actions[0].person_role, 'reader')
+ self.assertTrue(r.actions[0].group_name is None)
+ self.assertTrue(r.actions[0].group_role is None)
+ exp = """<request>
+ <action type="add_role">
+ <target package="bar" project="foo" />
+ <person name="user" role="reader" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_add_role_group(self):
+ """create an add_role request (group element)"""
+ r = osc.core.Request()
+ r.add_action('add_role', tgt_project='foo', tgt_package='bar', group_name='group', group_role='reviewer')
+ self.assertEqual(r.actions[0].type, 'add_role')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertEqual(r.actions[0].group_name, 'group')
+ self.assertEqual(r.actions[0].group_role, 'reviewer')
+ self.assertTrue(r.actions[0].person_name is None)
+ self.assertTrue(r.actions[0].person_role is None)
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="add_role">
+ <target package="bar" project="foo" />
+ <group name="group" role="reviewer" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_add_role_person_group(self):
+ """create an add_role request (person+group element)"""
+ r = osc.core.Request()
+ r.add_action('add_role', tgt_project='foo', tgt_package='bar', person_name='user', person_role='reader',
+ group_name='group', group_role='reviewer')
+ self.assertEqual(r.actions[0].type, 'add_role')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertEqual(r.actions[0].person_name, 'user')
+ self.assertEqual(r.actions[0].person_role, 'reader')
+ self.assertEqual(r.actions[0].group_name, 'group')
+ self.assertEqual(r.actions[0].group_role, 'reviewer')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="add_role">
+ <target package="bar" project="foo" />
+ <person name="user" role="reader" />
+ <group name="group" role="reviewer" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_set_bugowner_project(self):
+ """create a set_bugowner request for a project"""
+ r = osc.core.Request()
+ r.add_action('set_bugowner', tgt_project='foobar', person_name='buguser')
+ self.assertEqual(r.actions[0].type, 'set_bugowner')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertEqual(r.actions[0].person_name, 'buguser')
+ self.assertTrue(r.actions[0].tgt_package is None)
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="set_bugowner">
+ <target project="foobar" />
+ <person name="buguser" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_set_bugowner_package(self):
+ """create a set_bugowner request for a package"""
+ r = osc.core.Request()
+ r.add_action('set_bugowner', tgt_project='foobar', tgt_package='baz', person_name='buguser')
+ self.assertEqual(r.actions[0].type, 'set_bugowner')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertEqual(r.actions[0].tgt_package, 'baz')
+ self.assertEqual(r.actions[0].person_name, 'buguser')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="set_bugowner">
+ <target package="baz" project="foobar" />
+ <person name="buguser" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_delete_project(self):
+ """create a delete request for a project"""
+ r = osc.core.Request()
+ r.add_action('delete', tgt_project='foo')
+ self.assertEqual(r.actions[0].type, 'delete')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertTrue(r.actions[0].tgt_package is None)
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="delete">
+ <target project="foo" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_delete_package(self):
+ """create a delete request for a package"""
+ r = osc.core.Request()
+ r.add_action('delete', tgt_project='foo', tgt_package='deleteme')
+ self.assertEqual(r.actions[0].type, 'delete')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].tgt_package, 'deleteme')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="delete">
+ <target package="deleteme" project="foo" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_create_change_devel(self):
+ """create a change devel request"""
+ r = osc.core.Request()
+ r.add_action('change_devel', src_project='foo', src_package='bar', tgt_project='devprj', tgt_package='devpkg')
+ self.assertEqual(r.actions[0].type, 'change_devel')
+ self.assertEqual(r.actions[0].src_project, 'foo')
+ self.assertEqual(r.actions[0].src_package, 'bar')
+ self.assertEqual(r.actions[0].tgt_project, 'devprj')
+ self.assertEqual(r.actions[0].tgt_package, 'devpkg')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ exp = """<request>
+ <action type="change_devel">
+ <source package="bar" project="foo" />
+ <target package="devpkg" project="devprj" />
+ </action>
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_action_from_xml1(self):
+ """create action from xml"""
+ from xml.etree import cElementTree as ET
+ xml = """<action type="add_role">
+ <target package="bar" project="foo" />
+ <person name="user" role="reader" />
+ <group name="group" role="reviewer" />
+</action>"""
+ action = osc.core.Action.from_xml(ET.fromstring(xml))
+ self.assertEqual(action.type, 'add_role')
+ self.assertEqual(action.tgt_project, 'foo')
+ self.assertEqual(action.tgt_package, 'bar')
+ self.assertEqual(action.person_name, 'user')
+ self.assertEqual(action.person_role, 'reader')
+ self.assertEqual(action.group_name, 'group')
+ self.assertEqual(action.group_role, 'reviewer')
+ self.assertEqual(xml, action.to_str())
+
+ def test_action_from_xml2(self):
+ """create action from xml"""
+ from xml.etree import cElementTree as ET
+ xml = """<action type="submit">
+ <source package="bar" project="foo" />
+ <target package="bar" project="foobar" />
+ <options>
+ <sourceupdate>cleanup</sourceupdate>
+ <updatelink>1</updatelink>
+ </options>
+</action>"""
+ action = osc.core.Action.from_xml(ET.fromstring(xml))
+ self.assertEqual(action.type, 'submit')
+ self.assertEqual(action.src_project, 'foo')
+ self.assertEqual(action.src_package, 'bar')
+ self.assertEqual(action.tgt_project, 'foobar')
+ self.assertEqual(action.tgt_package, 'bar')
+ self.assertEqual(action.opt_sourceupdate, 'cleanup')
+ self.assertEqual(action.opt_updatelink, '1')
+ self.assertTrue(action.src_rev is None)
+ self.assertEqual(xml, action.to_str())
+
+ def test_action_from_xml3(self):
+ """create action from xml (with acceptinfo element)"""
+ from xml.etree import cElementTree as ET
+ xml = """<action type="submit">
+ <source package="bar" project="testprj" />
+ <target package="baz" project="foobar" />
+ <acceptinfo rev="5" srcmd5="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" xsrcmd5="ffffffffffffffffffffffffffffffff" />
+</action>"""
+ action = osc.core.Action.from_xml(ET.fromstring(xml))
+ self.assertEqual(action.type, 'submit')
+ self.assertEqual(action.src_project, 'testprj')
+ self.assertEqual(action.src_package, 'bar')
+ self.assertEqual(action.tgt_project, 'foobar')
+ self.assertEqual(action.tgt_package, 'baz')
+ self.assertTrue(action.opt_sourceupdate is None)
+ self.assertTrue(action.opt_updatelink is None)
+ self.assertTrue(action.src_rev is None)
+ self.assertEqual(action.acceptinfo_rev, '5')
+ self.assertEqual(action.acceptinfo_srcmd5, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+ self.assertEqual(action.acceptinfo_xsrcmd5, 'ffffffffffffffffffffffffffffffff')
+ self.assertTrue(action.acceptinfo_osrcmd5 is None)
+ self.assertTrue(action.acceptinfo_oxsrcmd5 is None)
+ self.assertEqual(xml, action.to_str())
+
+ def test_action_from_xml_unknown_type(self):
+ """try to create action from xml with unknown type"""
+ from xml.etree import cElementTree as ET
+ xml = '<action type="foo"><source package="bar" project="foo" /></action>'
+ self.assertRaises(osc.oscerr.WrongArgs, osc.core.Action.from_xml, ET.fromstring(xml))
+
+ def test_read_request1(self):
+ """read in a request"""
+ from xml.etree import cElementTree as ET
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_read_request1.xml'), 'r').read().strip()
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.reqid, '42')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'foo')
+ self.assertEqual(r.actions[0].src_package, 'bar')
+ self.assertEqual(r.actions[0].src_rev, '1')
+ self.assertEqual(r.actions[0].tgt_project, 'foobar')
+ self.assertEqual(r.actions[0].tgt_package, 'bar')
+ self.assertTrue(r.actions[0].opt_sourceupdate is None)
+ self.assertTrue(r.actions[0].opt_updatelink is None)
+ self.assertEqual(r.actions[1].type, 'delete')
+ self.assertEqual(r.actions[1].tgt_project, 'deleteme')
+ self.assertTrue(r.actions[1].tgt_package is None)
+ self.assertEqual(r.state.name, 'accepted')
+ self.assertEqual(r.state.when, '2010-12-27T01:36:29')
+ self.assertEqual(r.state.who, 'user1')
+ self.assertEqual(r.state.comment, '')
+ self.assertEqual(r.statehistory[0].when, '2010-12-13T13:02:03')
+ self.assertEqual(r.statehistory[0].who, 'creator')
+ self.assertEqual(r.statehistory[0].comment, 'foobar')
+ self.assertEqual(r.title, 'title of the request')
+ self.assertEqual(r.description, 'this is a\nvery long\ndescription')
+ self.assertTrue(len(r.statehistory) == 1)
+ self.assertTrue(len(r.reviews) == 0)
+ self.assertEqual(xml, r.to_str())
+
+ def test_read_request2(self):
+ """read in a request (with reviews)"""
+ from xml.etree import cElementTree as ET
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_read_request2.xml'), 'r').read().strip()
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.reqid, '123')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'xyz')
+ self.assertEqual(r.actions[0].src_package, 'abc')
+ self.assertTrue(r.actions[0].src_rev is None)
+ self.assertEqual(r.actions[0].opt_sourceupdate, 'cleanup')
+ self.assertEqual(r.actions[0].opt_updatelink, '1')
+ self.assertEqual(r.actions[1].type, 'add_role')
+ self.assertEqual(r.actions[1].tgt_project, 'home:foo')
+ self.assertEqual(r.actions[1].person_name, 'bar')
+ self.assertEqual(r.actions[1].person_role, 'maintainer')
+ self.assertEqual(r.actions[1].group_name, 'groupxyz')
+ self.assertEqual(r.actions[1].group_role, 'reader')
+ self.assertTrue(r.actions[1].tgt_package is None)
+ self.assertEqual(r.state.name, 'review')
+ self.assertEqual(r.state.when, '2010-12-27T01:36:29')
+ self.assertEqual(r.state.who, 'abc')
+ self.assertEqual(r.state.comment, '')
+ self.assertEqual(r.reviews[0].state, 'new')
+ self.assertEqual(r.reviews[0].by_group, 'group1')
+ self.assertEqual(r.reviews[0].when, '2010-12-28T00:11:22')
+ self.assertEqual(r.reviews[0].who, 'abc')
+ self.assertEqual(r.reviews[0].comment, 'review start')
+ self.assertTrue(r.reviews[0].by_user is None)
+ self.assertEqual(r.statehistory[0].when, '2010-12-11T00:00:00')
+ self.assertEqual(r.statehistory[0].who, 'creator')
+ self.assertEqual(r.statehistory[0].comment, '')
+ self.assertEqual(r.get_creator(), 'creator')
+ self.assertTrue(len(r.statehistory) == 1)
+ self.assertTrue(len(r.reviews) == 1)
+ self.assertEqual(xml, r.to_str())
+
+ def test_read_request3(self):
+ """read in a request (with an "empty" comment+description)"""
+ from xml.etree import cElementTree as ET
+ xml = """<request id="2">
+ <action type="set_bugowner">
+ <target project="foo" />
+ <person name="buguser" />
+ </action>
+ <state name="new" when="2010-12-28T12:36:29" who="xyz">
+ <comment></comment>
+ </state>
+ <description></description>
+</request>"""
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.reqid, '2')
+ self.assertEqual(r.actions[0].type, 'set_bugowner')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].person_name, 'buguser')
+ self.assertEqual(r.state.name, 'new')
+ self.assertEqual(r.state.when, '2010-12-28T12:36:29')
+ self.assertEqual(r.state.who, 'xyz')
+ self.assertEqual(r.state.comment, '')
+ self.assertEqual(r.description, '')
+ self.assertTrue(len(r.statehistory) == 0)
+ self.assertTrue(len(r.reviews) == 0)
+ self.assertEqual(r.get_creator(), 'xyz')
+ exp = """<request id="2">
+ <action type="set_bugowner">
+ <target project="foo" />
+ <person name="buguser" />
+ </action>
+ <state name="new" when="2010-12-28T12:36:29" who="xyz" />
+</request>"""
+
+ self.assertEqual(exp, r.to_str())
+
+ def test_request_list_view1(self):
+ """test the list_view method"""
+ from xml.etree import cElementTree as ET
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_request_list_view1.xml'), 'r').read().strip()
+ exp = """\
+ 62 State:new By:Admin When:2010-12-29T14:57:25
+ set_bugowner: buguser foo
+ add_role: person: xyz as maintainer, group: group1 as reader foobar
+ add_role: person: abc as reviewer foo/bar
+ change_devel: foo/bar developed in devprj/devpkg
+ submit: srcprj/srcpackage -> tgtprj/tgtpackage
+ submit: foo/bar -> baz
+ delete: deleteme
+ delete: foo/bar\n"""
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(exp, r.list_view())
+
+ def test_request_list_view2(self):
+ """test the list_view method (with history elements and description)"""
+ from xml.etree import cElementTree as ET
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_request_list_view2.xml'), 'r').read().strip()
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ exp = """\
+ 21 State:accepted By:foobar When:2010-12-29T16:37:45
+ set_bugowner: buguser foo
+ From: Created Request: user -> Review Approved: foobar
+ Descr: This is a simple request with a lot of ... ... text and other
+ stuff. This request also contains a description. This is useful
+ to describe the request. blabla blabla\n"""
+ self.assertEqual(exp, r.list_view())
+
+ def test_request_str1(self):
+ from xml.etree import cElementTree as ET
+ """test the __str__ method"""
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_request_str1.xml'), 'r').read().strip()
+ r = osc.core.Request()
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.get_creator(), 'creator')
+ exp = """\
+Request: #123
+
+ submit: xyz/abc(cleanup) -> foo ***update link***
+ add_role: person: bar as maintainer, group: groupxyz as reader home:foo
+
+
+Message:
+just a samll description
+in order to describe this
+request - blablabla
+test.
+
+State: review 2010-12-27T01:36:29 abc
+Comment: currently in review
+
+Review: accepted Group: group1 2010-12-29T00:11:22 abc
+ accepted
+ new Group: group1 2010-12-28T00:11:22 abc
+ review start
+
+History: 2010-12-12T00:00:00 creator revoked
+ 2010-12-11T00:00:00 creator new"""
+ self.assertEqual(exp, str(r))
+
+ def test_request_str2(self):
+ """test the __str__ method"""
+ from xml.etree import cElementTree as ET
+ xml = """\
+<request id="98765">
+ <action type="change_devel">
+ <source project="devprj" package="devpkg" />
+ <target project="foo" package="bar" />
+ </action>
+ <action type="delete">
+ <target project="deleteme" />
+ </action>
+ <state name="new" when="2010-12-29T00:11:22" who="creator" />
+</request>"""
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.get_creator(), 'creator')
+ exp = """\
+Request: #98765
+
+ change_devel: foo/bar developed in devprj/devpkg
+ delete: deleteme
+
+
+Message:
+<no message>
+
+State: new 2010-12-29T00:11:22 creator
+Comment: <no comment>"""
+ self.assertEqual(exp, str(r))
+
+ def test_legacy_request(self):
+ """load old-style submitrequest"""
+ from xml.etree import cElementTree as ET
+ xml = """\
+<request id="1234" type="submit">
+ <submit>
+ <source package="baz" project="foobar" />
+ <target package="baz" project="foo" />
+ </submit>
+ <state name="new" when="2010-12-30T02:11:22" who="olduser" />
+</request>"""
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ self.assertEqual(r.reqid, '1234')
+ self.assertEqual(r.actions[0].type, 'submit')
+ self.assertEqual(r.actions[0].src_project, 'foobar')
+ self.assertEqual(r.actions[0].src_package, 'baz')
+ self.assertEqual(r.actions[0].tgt_project, 'foo')
+ self.assertEqual(r.actions[0].tgt_package, 'baz')
+ self.assertTrue(r.actions[0].opt_sourceupdate is None)
+ self.assertTrue(r.actions[0].opt_updatelink is None)
+ self.assertEqual(r.state.name, 'new')
+ self.assertEqual(r.state.when, '2010-12-30T02:11:22')
+ self.assertEqual(r.state.who, 'olduser')
+ self.assertEqual(r.state.comment, '')
+ self.assertEqual(r.get_creator(), 'olduser')
+ exp = """\
+<request id="1234">
+ <action type="submit">
+ <source package="baz" project="foobar" />
+ <target package="baz" project="foo" />
+ </action>
+ <state name="new" when="2010-12-30T02:11:22" who="olduser" />
+</request>"""
+ self.assertEqual(exp, r.to_str())
+
+ def test_get_actions(self):
+ """test get_actions method"""
+ from xml.etree import cElementTree as ET
+ xml = open(os.path.join(self._get_fixtures_dir(), 'test_request_list_view1.xml'), 'r').read().strip()
+ r = osc.core.Request()
+ r.read(ET.fromstring(xml))
+ sr_actions = r.get_actions('submit')
+ self.assertTrue(len(sr_actions) == 2)
+ for i in sr_actions:
+ self.assertEqual(i.type, 'submit')
+ self.assertTrue(len(r.get_actions('submit', 'delete', 'change_devel')) == 5)
+ self.assertTrue(len(r.get_actions()) == 8)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_revertfiles.py b/tests/test_revertfiles.py
new file mode 100644
index 0000000..5a8c443
--- /dev/null
+++ b/tests/test_revertfiles.py
@@ -0,0 +1,97 @@
+import osc.core
+import osc.oscerr
+import os
+from common import OscTestCase
+
+FIXTURES_DIR = os.path.join(os.getcwd(), 'revertfile_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestRevertFiles)
+
+class TestRevertFiles(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ def testRevertUnchanged(self):
+ """revert an unchanged file (state == ' ')"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self.assertRaises(osc.oscerr.OscIOError, p.revert, 'toadd2')
+ self._check_status(p, 'toadd2', '?')
+
+ def testRevertModified(self):
+ """revert a modified file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('nochange')
+ self.__check_file('nochange')
+ self._check_status(p, 'nochange', ' ')
+
+ def testRevertAdded(self):
+ """revert an added file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('toadd1')
+ self.assertTrue(os.path.exists('toadd1'))
+ self._check_addlist('replaced\naddedmissing\n')
+ self._check_status(p, 'toadd1', '?')
+
+ def testRevertDeleted(self):
+ """revert a deleted file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('somefile')
+ self.__check_file('somefile')
+ self._check_deletelist('deleted\n')
+ self._check_status(p, 'somefile', ' ')
+
+ def testRevertMissing(self):
+ """revert a missing (state == '!') file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('missing')
+ self.__check_file('missing')
+ self._check_status(p, 'missing', ' ')
+
+ def testRevertMissingAdded(self):
+ """revert a missing file which was added to the wc"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('addedmissing')
+ self._check_addlist('toadd1\nreplaced\n')
+ self.assertRaises(osc.oscerr.OscIOError, p.status, 'addedmissing')
+
+ def testRevertReplaced(self):
+ """revert a replaced (state == 'R') file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('replaced')
+ self.__check_file('replaced')
+ self._check_addlist('toadd1\naddedmissing\n')
+ self._check_status(p, 'replaced', ' ')
+
+ def testRevertConflict(self):
+ """revert a file which is in the conflict state"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ p.revert('foo')
+ self.__check_file('foo')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_in_conflict')))
+ self._check_status(p, 'foo', ' ')
+
+ def testRevertSkipped(self):
+ """revert a skipped file"""
+ self._change_to_pkg('simple')
+ p = osc.core.Package('.')
+ self.assertRaises(osc.oscerr.OscIOError, p.revert, 'skipped')
+
+ def __check_file(self, fname):
+ storefile = os.path.join('.osc', fname)
+ self.assertTrue(os.path.exists(fname))
+ self.assertTrue(os.path.exists(storefile))
+ self.assertEqual(open(fname, 'r').read(), open(storefile, 'r').read())
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_setlinkrev.py b/tests/test_setlinkrev.py
new file mode 100644
index 0000000..2e91f54
--- /dev/null
+++ b/tests/test_setlinkrev.py
@@ -0,0 +1,93 @@
+import osc.core
+import osc.oscerr
+import os
+from common import GET, PUT, OscTestCase
+FIXTURES_DIR = os.path.join(os.getcwd(), 'setlinkrev_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestSetLinkRev)
+
+class TestSetLinkRev(OscTestCase):
+ def setUp(self):
+ OscTestCase.setUp(self, copytree=False)
+
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ @GET('http://localhost/source/osctest/simple/_link', file='simple_link')
+ @GET('http://localhost/source/srcprj/srcpkg?rev=latest', file='simple_filesremote')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" rev="42" />', text='dummytext')
+ def test_simple1(self):
+ """a simple set_link_rev call without revision"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple')
+
+ @GET('http://localhost/source/osctest/simple/_link', file='simple_link')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" rev="42" />', text='dummytext')
+ def test_simple2(self):
+ """a simple set_link_rev call with revision"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', '42')
+
+ @GET('http://localhost/source/osctest/simple/_link', file='noproject_link')
+ @GET('http://localhost/source/osctest/srcpkg?rev=latest&expand=1', file='expandedsrc_filesremote')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" rev="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" vrev="1" />', text='dummytext')
+ def test_expandedsrc(self):
+ """expand src package"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', expand=True)
+
+ @GET('http://localhost/source/osctest/simple/_link', file='link_with_rev')
+ @GET('http://localhost/source/srcprj/srcpkg?rev=latest', file='simple_filesremote')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" rev="42" />', text='dummytext')
+ def test_existingrev(self):
+ """link already has a rev attribute, update it to current version"""
+ # we could also avoid the superfluous PUT
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple')
+
+ @GET('http://localhost/source/osctest/simple/_link', file='link_with_rev')
+ @GET('http://localhost/source/srcprj/srcpkg?rev=latest&expand=1', file='expandedsrc_filesremote')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" rev="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" vrev="1" />',
+ text='dummytext')
+ def test_expandexistingrev(self):
+ """link already has a rev attribute, update it to current version"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', expand=True)
+
+ @GET('http://localhost/source/osctest/simple/_link', file='simple_link')
+ @GET('http://localhost/source/srcprj/srcpkg?rev=latest&expand=1', text='conflict in file merge', code=400)
+ def test_linkerror(self):
+ """link is broken"""
+ try:
+ from urllib.error import HTTPError
+ except ImportError:
+ from urllib2 import HTTPError
+ # the backend returns status 400 if we try to expand a broken _link
+ self.assertRaises(HTTPError, osc.core.set_link_rev, 'http://localhost', 'osctest', 'simple', expand=True)
+
+ @GET('http://localhost/source/osctest/simple/_link', file='rev_link')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" />', text='dummytext')
+ def test_deleterev(self):
+ """delete rev attribute from link xml"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', revision=None)
+
+ @GET('http://localhost/source/osctest/simple/_link', file='md5_rev_link')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" />', text='dummytext')
+ def test_deleterev(self):
+ """delete rev and vrev attribute from link xml"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', revision=None)
+
+ @GET('http://localhost/source/osctest/simple/_link', file='simple_link')
+ @PUT('http://localhost/source/osctest/simple/_link',
+ exp='<link package="srcpkg" project="srcprj" />', text='dummytext')
+ def test_deleterevnonexistent(self):
+ """delete non existent rev attribute from link xml"""
+ osc.core.set_link_rev('http://localhost', 'osctest', 'simple', revision=None)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/test_update.py b/tests/test_update.py
new file mode 100644
index 0000000..aaffcab
--- /dev/null
+++ b/tests/test_update.py
@@ -0,0 +1,288 @@
+import osc.core
+import osc.oscerr
+import os
+import sys
+from common import GET, OscTestCase
+FIXTURES_DIR = os.path.join(os.getcwd(), 'update_fixtures')
+
+def suite():
+ import unittest
+ return unittest.makeSuite(TestUpdate)
+
+class TestUpdate(OscTestCase):
+ def _get_fixtures_dir(self):
+ return FIXTURES_DIR
+
+ @GET('http://localhost/source/osctest/simple?rev=latest', file='testUpdateNoChanges_files')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateNoChanges(self):
+ """update without any changes (the wc is the most recent version)"""
+ self._change_to_pkg('simple')
+ osc.core.Package('.').update()
+ self.assertEqual(sys.stdout.getvalue(), 'At revision 1.\n')
+
+ @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateNewFile_files')
+ @GET('http://localhost/source/osctest/simple/upstream_added?rev=2', file='testUpdateNewFile_upstream_added')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateNewFile(self):
+ """a new file was added to the remote package"""
+ self._change_to_pkg('simple')
+ osc.core.Package('.').update(rev=2)
+ exp = 'A upstream_added\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testUpdateNewFile_files')
+
+ @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateNewFileLocalExists_files')
+ def testUpdateNewFileLocalExists(self):
+ """
+ a new file was added to the remote package but the same (unversioned)
+ file exists locally
+ """
+ self._change_to_pkg('simple')
+ self.assertRaises(osc.oscerr.PackageFileConflict, osc.core.Package('.').update, rev=2)
+
+ @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateDeletedFile_files')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateDeletedFile(self):
+ """a file was deleted from the remote package"""
+ self._change_to_pkg('simple')
+ osc.core.Package('.').update(rev=2)
+ exp = 'D foo\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testUpdateDeletedFile_files')
+ self.assertFalse(os.path.exists('foo'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'foo')))
+
+ @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateUpstreamModifiedFile_files')
+ @GET('http://localhost/source/osctest/simple/foo?rev=2', file='testUpdateUpstreamModifiedFile_foo')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateUpstreamModifiedFile(self):
+ """a file was modified in the remote package (local file isn't modified)"""
+
+ self._change_to_pkg('simple')
+ osc.core.Package('.').update(rev=2)
+ exp = 'U foo\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testUpdateUpstreamModifiedFile_files')
+
+ @GET('http://localhost/source/osctest/conflict?rev=2', file='testUpdateConflict_files')
+ @GET('http://localhost/source/osctest/conflict/merge?rev=2', file='testUpdateConflict_merge')
+ @GET('http://localhost/source/osctest/conflict/_meta', file='meta.xml')
+ def testUpdateConflict(self):
+ """
+ a file was modified in the remote package (local file is also modified
+ and a merge isn't possible)
+ """
+ self._change_to_pkg('conflict')
+ osc.core.Package('.').update(rev=2)
+ exp = 'C merge\nAt revision 2.\n'
+ self._check_digests('testUpdateConflict_files')
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_conflictlist('merge\n')
+
+ @GET('http://localhost/source/osctest/already_in_conflict?rev=2', file='testUpdateAlreadyInConflict_files')
+ @GET('http://localhost/source/osctest/already_in_conflict/merge?rev=2', file='testUpdateAlreadyInConflict_merge')
+ @GET('http://localhost/source/osctest/already_in_conflict/_meta', file='meta.xml')
+ def testUpdateAlreadyInConflict(self):
+ """
+ a file was modified in the remote package (the local file is already in conflict)
+ """
+ self._change_to_pkg('already_in_conflict')
+ osc.core.Package('.').update(rev=2)
+ exp = 'skipping \'merge\' (this is due to conflicts)\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_conflictlist('merge\n')
+ self._check_digests('testUpdateAlreadyInConflict_files')
+
+ @GET('http://localhost/source/osctest/deleted?rev=2', file='testUpdateLocalDeletions_files')
+ @GET('http://localhost/source/osctest/deleted/foo?rev=2', file='testUpdateLocalDeletions_foo')
+ @GET('http://localhost/source/osctest/deleted/merge?rev=2', file='testUpdateLocalDeletions_merge')
+ @GET('http://localhost/source/osctest/deleted/_meta', file='meta.xml')
+ def testUpdateLocalDeletions(self):
+ """
+ the files 'foo' and 'merge' were modified in the remote package
+ and marked for deletion in the local wc. Additionally the file
+ 'merge' was modified in the wc before deletion so the local file
+ still exists (and a merge with the remote file is not possible)
+ """
+ self._change_to_pkg('deleted')
+ osc.core.Package('.').update(rev=2)
+ exp = 'U foo\nC merge\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_deletelist('foo\n')
+ self._check_conflictlist('merge\n')
+ self.assertEqual(open('foo', 'r').read(), open(os.path.join('.osc', 'foo'), 'r').read())
+ self._check_digests('testUpdateLocalDeletions_files')
+
+ @GET('http://localhost/source/osctest/restore?rev=latest', file='testUpdateRestore_files')
+ @GET('http://localhost/source/osctest/restore/foo?rev=1', file='testUpdateRestore_foo')
+ @GET('http://localhost/source/osctest/restore/_meta', file='meta.xml')
+ def testUpdateRestore(self):
+ """local file 'foo' was deleted with a non osc command and will be restored"""
+ self._change_to_pkg('restore')
+ osc.core.Package('.').update()
+ exp = 'Restored \'foo\'\nAt revision 1.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testUpdateRestore_files')
+
+ @GET('http://localhost/source/osctest/limitsize?rev=latest', file='testUpdateLimitSizeNoChange_filesremote')
+ @GET('http://localhost/source/osctest/limitsize/_meta', file='meta.xml')
+ def testUpdateLimitSizeNoChange(self):
+ """
+ a new file was added to the remote package but isn't checked out because
+ of the size constraint
+ """
+ self._change_to_pkg('limitsize')
+ osc.core.Package('.').update(size_limit=50)
+ exp = 'D bigfile\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile')))
+ self.assertFalse(os.path.exists('bigfile'))
+ self._check_digests('testUpdateLimitSizeNoChange_files', 'bigfile')
+
+ @GET('http://localhost/source/osctest/limitsize_local?rev=latest', file='testUpdateLocalLimitSizeNoChange_filesremote')
+ @GET('http://localhost/source/osctest/limitsize_local/_meta', file='meta.xml')
+ def testUpdateLocalLimitSizeNoChange(self):
+ """
+ a new file was added to the remote package but isn't checked out because
+ of the local size constraint
+ """
+ self._change_to_pkg('limitsize_local')
+ p = osc.core.Package('.')
+ p.update()
+ exp = 'D bigfile\nD merge\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile')))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'merge')))
+ self.assertFalse(os.path.exists('bigfile'))
+ self._check_digests('testUpdateLocalLimitSizeNoChange_files', 'bigfile', 'merge')
+ self._check_status(p, 'bigfile', 'S')
+ self._check_status(p, 'merge', 'S')
+
+ @GET('http://localhost/source/osctest/limitsize?rev=latest', file='testUpdateLimitSizeAddDelete_filesremote')
+ @GET('http://localhost/source/osctest/limitsize/exists?rev=2', file='testUpdateLimitSizeAddDelete_exists')
+ @GET('http://localhost/source/osctest/limitsize/_meta', file='meta.xml')
+ def testUpdateLimitSizeAddDelete(self):
+ """
+ a new file (exists) was added to the remote package with
+ size < size_limit and one file (nochange) was deleted from the
+ remote package (local file 'nochange' is modified). Additionally
+ files which didn't change are removed the local wc due to the
+ size constraint.
+ """
+ self._change_to_pkg('limitsize')
+ osc.core.Package('.').update(size_limit=10)
+ exp = 'A exists\nD bigfile\nD foo\nD merge\nD nochange\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile')))
+ self.assertFalse(os.path.exists('bigfile'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'foo')))
+ self.assertFalse(os.path.exists('foo'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'merge')))
+ self.assertFalse(os.path.exists('merge'))
+ # exists because local version is modified
+ self.assertTrue(os.path.exists('nochange'))
+
+ self._check_digests('testUpdateLimitSizeAddDelete_files', 'bigfile', 'foo', 'merge', 'nochange')
+
+ @GET('http://localhost/source/osctest/services?rev=latest', file='testUpdateServiceFilesAddDelete_filesremote')
+ @GET('http://localhost/source/osctest/services/bigfile?rev=2', file='testUpdateServiceFilesAddDelete_bigfile')
+ @GET('http://localhost/source/osctest/services/_service%3Abar?rev=2', file='testUpdateServiceFilesAddDelete__service:bar')
+ @GET('http://localhost/source/osctest/services/_service%3Afoo?rev=2', file='testUpdateServiceFilesAddDelete__service:foo')
+ @GET('http://localhost/source/osctest/services/_meta', file='meta.xml')
+ def testUpdateAddDeleteServiceFiles(self):
+ """update package with _service:* files"""
+ self._change_to_pkg('services')
+ osc.core.Package('.').update(service_files=True)
+ exp = 'A bigfile\nD _service:exists\nA _service:bar\nA _service:foo\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_service:bar')))
+ self.assertTrue(os.path.exists('_service:bar'))
+ self.assertEqual(open('_service:bar').read(), 'another service\n')
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_service:foo')))
+ self.assertTrue(os.path.exists('_service:foo'))
+ self.assertEqual(open('_service:foo').read(), 'small\n')
+ self.assertTrue(os.path.exists('_service:exists'))
+ self._check_digests('testUpdateServiceFilesAddDelete_files', '_service:foo', '_service:bar')
+
+ @GET('http://localhost/source/osctest/services?rev=latest', file='testUpdateServiceFilesAddDelete_filesremote')
+ @GET('http://localhost/source/osctest/services/bigfile?rev=2', file='testUpdateServiceFilesAddDelete_bigfile')
+ @GET('http://localhost/source/osctest/services/_meta', file='meta.xml')
+ def testUpdateDisableAddDeleteServiceFiles(self):
+ """update package with _service:* files (with service_files=False)"""
+ self._change_to_pkg('services')
+ osc.core.Package('.').update()
+ exp = 'A bigfile\nD _service:exists\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_service:bar')))
+ self.assertFalse(os.path.exists('_service:bar'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_service:foo')))
+ self.assertFalse(os.path.exists('_service:foo'))
+ self.assertTrue(os.path.exists('_service:exists'))
+ self._check_digests('testUpdateServiceFilesAddDelete_files', '_service:foo', '_service:bar')
+
+ @GET('http://localhost/source/osctest/metamode?meta=1&rev=latest', file='testUpdateMetaMode_filesremote')
+ @GET('http://localhost/source/osctest/metamode/_meta?rev=1', file='testUpdateMetaMode__meta')
+ def testUpdateMetaMode(self):
+ """update package with metamode enabled"""
+ self._change_to_pkg('metamode')
+ p = osc.core.Package('.')
+ p.update()
+ exp = 'A _meta\nD foo\nD merge\nD nochange\nAt revision 1.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists('foo'))
+ self.assertFalse(os.path.exists('merge'))
+ self.assertFalse(os.path.exists('nochange'))
+ self._check_digests('testUpdateMetaMode_filesremote')
+ self._check_status(p, '_meta', ' ')
+
+ @GET('http://localhost/source/osctest/new?rev=latest', file='testUpdateNew_filesremote')
+ @GET('http://localhost/source/osctest/new/_meta', file='meta.xml')
+ def testUpdateNew(self):
+ """update a new (empty) package. The package has no revision."""
+ self._change_to_pkg('new')
+ p = osc.core.Package('.')
+ p.update()
+ exp = 'At revision None.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self._check_digests('testUpdateNew_filesremote')
+
+ # tests to recover from an aborted/broken update
+
+ @GET('http://localhost/source/osctest/simple/foo?rev=2', file='testUpdateResume_foo')
+ @GET('http://localhost/source/osctest/simple/merge?rev=2', file='testUpdateResume_merge')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateResume_files')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateResume(self):
+ """resume an aborted update"""
+ self._change_to_pkg('resume')
+ osc.core.Package('.').update(rev=2)
+ exp = 'resuming broken update...\nU foo\nU merge\nAt revision 2.\nAt revision 2.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_in_update')))
+ self._check_digests('testUpdateResume_files')
+
+ @GET('http://localhost/source/osctest/simple/foo?rev=1', file='testUpdateResumeDeletedFile_foo')
+ @GET('http://localhost/source/osctest/simple/merge?rev=1', file='testUpdateResumeDeletedFile_merge')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ @GET('http://localhost/source/osctest/simple?rev=1', file='testUpdateResumeDeletedFile_files')
+ @GET('http://localhost/source/osctest/simple/_meta', file='meta.xml')
+ def testUpdateResumeDeletedFile(self):
+ """
+ resume an aborted update (the file 'added' was already deleted in the first update
+ run). It's marked as deleted again (this is due to an expected issue with the update
+ code)
+ """
+ self._change_to_pkg('resume_deleted')
+ osc.core.Package('.').update(rev=1)
+ exp = 'resuming broken update...\nD added\nU foo\nU merge\nAt revision 1.\nAt revision 1.\n'
+ self.assertEqual(sys.stdout.getvalue(), exp)
+ self.assertFalse(os.path.exists(os.path.join('.osc', '_in_update')))
+ self.assertFalse(os.path.exists('added'))
+ self.assertFalse(os.path.exists(os.path.join('.osc', 'added')))
+ self._check_digests('testUpdateResumeDeletedFile_files')
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/update_fixtures/meta.xml b/tests/update_fixtures/meta.xml
new file mode 100644
index 0000000..abd3720
--- /dev/null
+++ b/tests/update_fixtures/meta.xml
@@ -0,0 +1,8 @@
+<package project="osctest" name="simple">
+ <title/>
+ <description>
+
+ </description>
+ <person userid="Admin" role="maintainer"/>
+ <person userid="Admin" role="bugowner"/>
+</package> \ No newline at end of file
diff --git a/tests/update_fixtures/oscrc b/tests/update_fixtures/oscrc
new file mode 100644
index 0000000..a30e040
--- /dev/null
+++ b/tests/update_fixtures/oscrc
@@ -0,0 +1,103 @@
+[general]
+# URL to access API server, e.g. https://api.opensuse.org
+# you also need a section [https://api.opensuse.org] with the credentials
+apiurl = http://localhost
+# Downloaded packages are cached here. Must be writable by you.
+#packagecachedir = /var/tmp/osbuild-packagecache
+# Wrapper to call build as root (sudo, su -, ...)
+#su-wrapper = su -c
+# rootdir to setup the chroot environment
+# can contain %(repo)s, %(arch)s, %(project)s and %(package)s for replacement, e.g.
+# /srv/oscbuild/%(repo)s-%(arch)s or
+# /srv/oscbuild/%(repo)s-%(arch)s-%(project)s-%(package)s
+#build-root = /var/tmp/build-root
+# compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
+#build-jobs = N
+# build-type to use - values can be (depending on the capabilities of the 'build' script)
+# empty - chroot build
+# kvm - kvm VM build (needs build-device, build-swap, build-memory)
+# xen - xen VM build (needs build-device, build-swap, build-memory)
+# experimental:
+# qemu - qemu VM build
+# lxc - lxc build
+#build-type =
+# build-device is the disk-image file to use as root for VM builds
+# e.g. /var/tmp/FILE.root
+#build-device = /var/tmp/FILE.root
+# build-swap is the disk-image to use as swap for VM builds
+# e.g. /var/tmp/FILE.swap
+#build-swap = /var/tmp/FILE.swap
+# build-memory is the amount of memory used in the VM
+# value in MB - e.g. 512
+#build-memory = 512
+# build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
+# values in MB - e.g. 4096
+#build-vmdisk-rootsize = 4096
+# build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
+# values in MB - e.g. 1024
+#build-vmdisk-swapsize = 1024
+# Numeric uid:gid to assign to the "abuild" user in the build-root
+# or "caller" to use the current users uid:gid
+# This is convenient when sharing the buildroot with ordinary userids
+# on the host.
+# This should not be 0
+# build-uid =
+# extra packages to install when building packages locally (osc build)
+# this corresponds to osc build's -x option and can be overridden with that
+# -x '' can also be given on the command line to override this setting, or
+# you can have an empty setting here.
+#extra-pkgs = vim gdb strace
+# build platform is used if the platform argument is omitted to osc build
+#build_repository = openSUSE_Factory
+# default project for getpac or bco
+#getpac_default_project = openSUSE:Factory
+# alternate filesystem layout: have multiple subdirs, where colons were.
+#checkout_no_colon = 0
+# local files to ignore with status, addremove, ....
+#exclude_glob = .osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.*
+# keep passwords in plaintext. If you see this comment, your osc
+# already uses the encrypted password, and only keeps them in plain text
+# for backwards compatibility. Default will change to 0 in future releases.
+# You can remove the plaintext password without harm, if you do not need
+# backwards compatibility.
+#plaintext_passwd = 1
+# limit the age of requests shown with 'osc req list'.
+# this is a default only, can be overridden by 'osc req list -D NNN'
+# Use 0 for unlimted.
+#request_list_days = 0
+# show info useful for debugging
+#debug = 1
+# show HTTP traffic useful for debugging
+#http_debug = 1
+# Skip signature verification of packages used for build.
+#no_verify = 1
+# jump into the debugger in case of errors
+#post_mortem = 1
+# print call traces in case of errors
+#traceback = 1
+# use KDE/Gnome/MacOS/Windows keyring for credentials if available
+#use_keyring = 1
+# check for unversioned/removed files before commit
+#check_filelist = 1
+# check for pending requests after executing an action (e.g. checkout, update, commit)
+#check_for_request_on_action = 0
+# what to do with the source package if the submitrequest has been accepted. If
+# nothing is specified the API default is used
+#submitrequest_on_accept_action = cleanup|update|noupdate
+#review requests interactively (default: off)
+#request_show_review = 1
+# Directory with executables to validate sources, esp before committing
+#source_validator_directory = /usr/lib/osc/source_validators
+
+[http://localhost]
+user=Admin
+pass=opensuse
+# set aliases for this apiurl
+# aliases = foo, bar
+# email used in .changes, unless the one from osc meta prj <user> will be used
+# email =
+# additional headers to pass to a request, e.g. for special authentication
+#http_headers = Host: foofoobar,
+# User: mumblegack
+# Force using of keyring for this API
+#keyring = 1
diff --git a/tests/update_fixtures/osctest/.osc/_apiurl b/tests/update_fixtures/osctest/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/.osc/_packages b/tests/update_fixtures/osctest/.osc/_packages
new file mode 100644
index 0000000..04e56f0
--- /dev/null
+++ b/tests/update_fixtures/osctest/.osc/_packages
@@ -0,0 +1 @@
+<project name="osctest" />
diff --git a/tests/update_fixtures/osctest/.osc/_project b/tests/update_fixtures/osctest/.osc/_project
new file mode 100644
index 0000000..b83ffd3
--- /dev/null
+++ b/tests/update_fixtures/osctest/.osc/_project
@@ -0,0 +1 @@
+osctest
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_apiurl b/tests/update_fixtures/osctest/already_in_conflict/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_files b/tests/update_fixtures/osctest/already_in_conflict/.osc/_files
new file mode 100644
index 0000000..2ad5954
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="already_in_conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282133912" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282133912" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282133912" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_in_conflict b/tests/update_fixtures/osctest/already_in_conflict/.osc/_in_conflict
new file mode 100644
index 0000000..a00af07
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_in_conflict
@@ -0,0 +1 @@
+merge
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_meta b/tests/update_fixtures/osctest/already_in_conflict/.osc/_meta
new file mode 100644
index 0000000..0150d60
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_meta
@@ -0,0 +1,8 @@
+<package project="osctest" name="already_in_conflict">
+ <title/>
+ <description>
+
+ </description>
+ <person userid="Admin" role="maintainer"/>
+ <person userid="Admin" role="bugowner"/>
+</package> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_osclib_version b/tests/update_fixtures/osctest/already_in_conflict/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_package b/tests/update_fixtures/osctest/already_in_conflict/.osc/_package
new file mode 100644
index 0000000..c2cae8d
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_package
@@ -0,0 +1 @@
+already_in_conflict \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/_project b/tests/update_fixtures/osctest/already_in_conflict/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/foo b/tests/update_fixtures/osctest/already_in_conflict/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/merge b/tests/update_fixtures/osctest/already_in_conflict/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/already_in_conflict/.osc/nochange b/tests/update_fixtures/osctest/already_in_conflict/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/already_in_conflict/foo b/tests/update_fixtures/osctest/already_in_conflict/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/already_in_conflict/merge b/tests/update_fixtures/osctest/already_in_conflict/merge
new file mode 100644
index 0000000..7469d51
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/merge
@@ -0,0 +1,2 @@
+Is it
+I hope so...
diff --git a/tests/update_fixtures/osctest/already_in_conflict/nochange b/tests/update_fixtures/osctest/already_in_conflict/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/already_in_conflict/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/conflict/.osc/_apiurl b/tests/update_fixtures/osctest/conflict/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/conflict/.osc/_files b/tests/update_fixtures/osctest/conflict/.osc/_files
new file mode 100644
index 0000000..a67ff42
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282130148" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282130148" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/conflict/.osc/_osclib_version b/tests/update_fixtures/osctest/conflict/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/conflict/.osc/_package b/tests/update_fixtures/osctest/conflict/.osc/_package
new file mode 100644
index 0000000..783a0ef
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/_package
@@ -0,0 +1 @@
+conflict \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/conflict/.osc/_project b/tests/update_fixtures/osctest/conflict/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/conflict/.osc/foo b/tests/update_fixtures/osctest/conflict/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/conflict/.osc/merge b/tests/update_fixtures/osctest/conflict/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/conflict/.osc/nochange b/tests/update_fixtures/osctest/conflict/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/conflict/foo b/tests/update_fixtures/osctest/conflict/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/conflict/merge b/tests/update_fixtures/osctest/conflict/merge
new file mode 100644
index 0000000..f4ff164
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/merge
@@ -0,0 +1,4 @@
+Is it possible
+to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/conflict/nochange b/tests/update_fixtures/osctest/conflict/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/conflict/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_apiurl b/tests/update_fixtures/osctest/deleted/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_files b/tests/update_fixtures/osctest/deleted/.osc/_files
new file mode 100644
index 0000000..d9a5451
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="deleted" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282134731" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282134731" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282134731" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_osclib_version b/tests/update_fixtures/osctest/deleted/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_package b/tests/update_fixtures/osctest/deleted/.osc/_package
new file mode 100644
index 0000000..3c22137
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_package
@@ -0,0 +1 @@
+deleted \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_project b/tests/update_fixtures/osctest/deleted/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/deleted/.osc/_to_be_deleted b/tests/update_fixtures/osctest/deleted/.osc/_to_be_deleted
new file mode 100644
index 0000000..fa7a1f7
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/_to_be_deleted
@@ -0,0 +1,2 @@
+merge
+foo
diff --git a/tests/update_fixtures/osctest/deleted/.osc/foo b/tests/update_fixtures/osctest/deleted/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/deleted/.osc/merge b/tests/update_fixtures/osctest/deleted/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/deleted/.osc/nochange b/tests/update_fixtures/osctest/deleted/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/deleted/merge b/tests/update_fixtures/osctest/deleted/merge
new file mode 100644
index 0000000..c229519
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/merge
@@ -0,0 +1,3 @@
+Is it possible to,
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/deleted/nochange b/tests/update_fixtures/osctest/deleted/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/deleted/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/_apiurl b/tests/update_fixtures/osctest/limitsize/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/_files b/tests/update_fixtures/osctest/limitsize/.osc/_files
new file mode 100644
index 0000000..77d67af
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="limitsize" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/_osclib_version b/tests/update_fixtures/osctest/limitsize/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/_package b/tests/update_fixtures/osctest/limitsize/.osc/_package
new file mode 100644
index 0000000..edc7cc1
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/_package
@@ -0,0 +1 @@
+limitsize
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/_project b/tests/update_fixtures/osctest/limitsize/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/foo b/tests/update_fixtures/osctest/limitsize/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/merge b/tests/update_fixtures/osctest/limitsize/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/limitsize/.osc/nochange b/tests/update_fixtures/osctest/limitsize/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/limitsize/foo b/tests/update_fixtures/osctest/limitsize/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/limitsize/merge b/tests/update_fixtures/osctest/limitsize/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/limitsize/nochange b/tests/update_fixtures/osctest/limitsize/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_apiurl b/tests/update_fixtures/osctest/limitsize_local/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_files b/tests/update_fixtures/osctest/limitsize_local/.osc/_files
new file mode 100644
index 0000000..77d67af
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="limitsize" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_osclib_version b/tests/update_fixtures/osctest/limitsize_local/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_package b/tests/update_fixtures/osctest/limitsize_local/.osc/_package
new file mode 100644
index 0000000..64a5ed3
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_package
@@ -0,0 +1 @@
+limitsize_local
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_project b/tests/update_fixtures/osctest/limitsize_local/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/_size_limit b/tests/update_fixtures/osctest/limitsize_local/.osc/_size_limit
new file mode 100644
index 0000000..64bb6b7
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/_size_limit
@@ -0,0 +1 @@
+30
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/foo b/tests/update_fixtures/osctest/limitsize_local/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/merge b/tests/update_fixtures/osctest/limitsize_local/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/limitsize_local/.osc/nochange b/tests/update_fixtures/osctest/limitsize_local/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/limitsize_local/foo b/tests/update_fixtures/osctest/limitsize_local/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/limitsize_local/merge b/tests/update_fixtures/osctest/limitsize_local/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/limitsize_local/nochange b/tests/update_fixtures/osctest/limitsize_local/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/update_fixtures/osctest/limitsize_local/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_apiurl b/tests/update_fixtures/osctest/metamode/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_files b/tests/update_fixtures/osctest/metamode/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_meta_mode b/tests/update_fixtures/osctest/metamode/.osc/_meta_mode
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_meta_mode
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_osclib_version b/tests/update_fixtures/osctest/metamode/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_package b/tests/update_fixtures/osctest/metamode/.osc/_package
new file mode 100644
index 0000000..862084f
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_package
@@ -0,0 +1 @@
+metamode
diff --git a/tests/update_fixtures/osctest/metamode/.osc/_project b/tests/update_fixtures/osctest/metamode/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/metamode/.osc/foo b/tests/update_fixtures/osctest/metamode/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/metamode/.osc/merge b/tests/update_fixtures/osctest/metamode/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/metamode/.osc/nochange b/tests/update_fixtures/osctest/metamode/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/metamode/foo b/tests/update_fixtures/osctest/metamode/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/metamode/merge b/tests/update_fixtures/osctest/metamode/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/metamode/nochange b/tests/update_fixtures/osctest/metamode/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/metamode/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/new/.osc/_apiurl b/tests/update_fixtures/osctest/new/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/new/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/new/.osc/_files b/tests/update_fixtures/osctest/new/.osc/_files
new file mode 100644
index 0000000..d915967
--- /dev/null
+++ b/tests/update_fixtures/osctest/new/.osc/_files
@@ -0,0 +1 @@
+<directory name="new" />
diff --git a/tests/update_fixtures/osctest/new/.osc/_osclib_version b/tests/update_fixtures/osctest/new/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/new/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/new/.osc/_package b/tests/update_fixtures/osctest/new/.osc/_package
new file mode 100644
index 0000000..3e75765
--- /dev/null
+++ b/tests/update_fixtures/osctest/new/.osc/_package
@@ -0,0 +1 @@
+new
diff --git a/tests/update_fixtures/osctest/new/.osc/_project b/tests/update_fixtures/osctest/new/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/new/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/restore/.osc/_apiurl b/tests/update_fixtures/osctest/restore/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/restore/.osc/_files b/tests/update_fixtures/osctest/restore/.osc/_files
new file mode 100644
index 0000000..a6b0cc6
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="restore" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/restore/.osc/_osclib_version b/tests/update_fixtures/osctest/restore/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/restore/.osc/_package b/tests/update_fixtures/osctest/restore/.osc/_package
new file mode 100644
index 0000000..a9db91d
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/_package
@@ -0,0 +1 @@
+restore
diff --git a/tests/update_fixtures/osctest/restore/.osc/_project b/tests/update_fixtures/osctest/restore/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/restore/.osc/foo b/tests/update_fixtures/osctest/restore/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/restore/.osc/merge b/tests/update_fixtures/osctest/restore/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/restore/.osc/nochange b/tests/update_fixtures/osctest/restore/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/restore/exists b/tests/update_fixtures/osctest/restore/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/exists
diff --git a/tests/update_fixtures/osctest/restore/merge b/tests/update_fixtures/osctest/restore/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/restore/nochange b/tests/update_fixtures/osctest/restore/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/restore/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/resume/.osc/_apiurl b/tests/update_fixtures/osctest/resume/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/resume/.osc/_files b/tests/update_fixtures/osctest/resume/.osc/_files
new file mode 100644
index 0000000..e4f249e
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="ff22941336956098ae9a564289d1bf1b" mtime="1282137256" name="added" size="15" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/resume/.osc/_in_update/_files b/tests/update_fixtures/osctest/resume/.osc/_in_update/_files
new file mode 100644
index 0000000..0b0a0c8
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_in_update/_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="3ac41c59a5ed169d5ffef4d824700f7d" vrev="2">
+ <entry md5="ff22941336956098ae9a564289d1bf1b" mtime="1282137256" name="added" size="15" />
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" mtime="1282137220" name="foo" size="7" />
+ <entry md5="256d8f76ba7a0a231fb46a84866f25d8" mtime="1282137238" name="merge" size="20" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/resume/.osc/_in_update/foo b/tests/update_fixtures/osctest/resume/.osc/_in_update/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_in_update/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/resume/.osc/_meta b/tests/update_fixtures/osctest/resume/.osc/_meta
new file mode 100644
index 0000000..abd3720
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_meta
@@ -0,0 +1,8 @@
+<package project="osctest" name="simple">
+ <title/>
+ <description>
+
+ </description>
+ <person userid="Admin" role="maintainer"/>
+ <person userid="Admin" role="bugowner"/>
+</package> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume/.osc/_osclib_version b/tests/update_fixtures/osctest/resume/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/resume/.osc/_package b/tests/update_fixtures/osctest/resume/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume/.osc/_project b/tests/update_fixtures/osctest/resume/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume/.osc/added b/tests/update_fixtures/osctest/resume/.osc/added
new file mode 100644
index 0000000..0527e6b
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/added
@@ -0,0 +1 @@
+This is a test
diff --git a/tests/update_fixtures/osctest/resume/.osc/foo b/tests/update_fixtures/osctest/resume/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/resume/.osc/merge b/tests/update_fixtures/osctest/resume/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/resume/.osc/nochange b/tests/update_fixtures/osctest/resume/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/resume/added b/tests/update_fixtures/osctest/resume/added
new file mode 100644
index 0000000..0527e6b
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/added
@@ -0,0 +1 @@
+This is a test
diff --git a/tests/update_fixtures/osctest/resume/exists b/tests/update_fixtures/osctest/resume/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/exists
diff --git a/tests/update_fixtures/osctest/resume/foo b/tests/update_fixtures/osctest/resume/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/resume/merge b/tests/update_fixtures/osctest/resume/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/resume/nochange b/tests/update_fixtures/osctest/resume/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_apiurl b/tests/update_fixtures/osctest/resume_deleted/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_files b/tests/update_fixtures/osctest/resume_deleted/.osc/_files
new file mode 100644
index 0000000..5796136
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="1" srcmd5="3ac41c59a5ed169d5ffef4d824700f7d" vrev="1">
+ <entry md5="d41d8cd98f00b204e9800998ecf8427e" mtime="1282137256" name="added" size="15" />
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" mtime="1282137220" name="foo" size="7" />
+ <entry md5="256d8f76ba7a0a231fb46a84866f25d8" mtime="1282137238" name="merge" size="20" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/_files b/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/foo b/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/foo
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_in_update/foo
@@ -0,0 +1 @@
+foobar
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_meta b/tests/update_fixtures/osctest/resume_deleted/.osc/_meta
new file mode 100644
index 0000000..abd3720
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_meta
@@ -0,0 +1,8 @@
+<package project="osctest" name="simple">
+ <title/>
+ <description>
+
+ </description>
+ <person userid="Admin" role="maintainer"/>
+ <person userid="Admin" role="bugowner"/>
+</package> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_osclib_version b/tests/update_fixtures/osctest/resume_deleted/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_package b/tests/update_fixtures/osctest/resume_deleted/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/_project b/tests/update_fixtures/osctest/resume_deleted/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/added b/tests/update_fixtures/osctest/resume_deleted/.osc/added
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/added
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/foo b/tests/update_fixtures/osctest/resume_deleted/.osc/foo
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/foo
@@ -0,0 +1 @@
+foobar
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/merge b/tests/update_fixtures/osctest/resume_deleted/.osc/merge
new file mode 100644
index 0000000..2563d89
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/merge
@@ -0,0 +1,5 @@
+xxx
+xxx
+yyy
+zzz
+zzz
diff --git a/tests/update_fixtures/osctest/resume_deleted/.osc/nochange b/tests/update_fixtures/osctest/resume_deleted/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/resume_deleted/added b/tests/update_fixtures/osctest/resume_deleted/added
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/added
diff --git a/tests/update_fixtures/osctest/resume_deleted/exists b/tests/update_fixtures/osctest/resume_deleted/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/exists
diff --git a/tests/update_fixtures/osctest/resume_deleted/f b/tests/update_fixtures/osctest/resume_deleted/f
new file mode 100644
index 0000000..0527e6b
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/f
@@ -0,0 +1 @@
+This is a test
diff --git a/tests/update_fixtures/osctest/resume_deleted/foo b/tests/update_fixtures/osctest/resume_deleted/foo
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/foo
@@ -0,0 +1 @@
+foobar
diff --git a/tests/update_fixtures/osctest/resume_deleted/merge b/tests/update_fixtures/osctest/resume_deleted/merge
new file mode 100644
index 0000000..2563d89
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/merge
@@ -0,0 +1,5 @@
+xxx
+xxx
+yyy
+zzz
+zzz
diff --git a/tests/update_fixtures/osctest/resume_deleted/nochange b/tests/update_fixtures/osctest/resume_deleted/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/resume_deleted/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/services/.osc/_apiurl b/tests/update_fixtures/osctest/services/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/services/.osc/_files b/tests/update_fixtures/osctest/services/.osc/_files
new file mode 100644
index 0000000..9209ca9
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="foo" rev="1" srcmd5="b9f060f4b3640e58a1d44abc25ffb9bd" vrev="1">
+ <entry md5="7b1458c733a187d4f3807665ddd02cca" mtime="1282565027" name="_service:exists" size="20" skipped="true" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282320303" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282320303" name="merge" size="48" />
+</directory>
diff --git a/tests/update_fixtures/osctest/services/.osc/_osclib_version b/tests/update_fixtures/osctest/services/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/services/.osc/_package b/tests/update_fixtures/osctest/services/.osc/_package
new file mode 100644
index 0000000..f7a48f2
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/_package
@@ -0,0 +1 @@
+services
diff --git a/tests/update_fixtures/osctest/services/.osc/_project b/tests/update_fixtures/osctest/services/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/services/.osc/foo b/tests/update_fixtures/osctest/services/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/services/.osc/merge b/tests/update_fixtures/osctest/services/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/services/_service:exists b/tests/update_fixtures/osctest/services/_service:exists
new file mode 100644
index 0000000..85e1c2f
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/_service:exists
@@ -0,0 +1,2 @@
+another service
+foo
diff --git a/tests/update_fixtures/osctest/services/foo b/tests/update_fixtures/osctest/services/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/services/merge b/tests/update_fixtures/osctest/services/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/services/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/simple/.osc/_apiurl b/tests/update_fixtures/osctest/simple/.osc/_apiurl
new file mode 100644
index 0000000..0afeace
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/_apiurl
@@ -0,0 +1 @@
+http://localhost
diff --git a/tests/update_fixtures/osctest/simple/.osc/_files b/tests/update_fixtures/osctest/simple/.osc/_files
new file mode 100644
index 0000000..f0dac1f
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory> \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/simple/.osc/_osclib_version b/tests/update_fixtures/osctest/simple/.osc/_osclib_version
new file mode 100644
index 0000000..d3827e7
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/_osclib_version
@@ -0,0 +1 @@
+1.0
diff --git a/tests/update_fixtures/osctest/simple/.osc/_package b/tests/update_fixtures/osctest/simple/.osc/_package
new file mode 100644
index 0000000..8fd3246
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/_package
@@ -0,0 +1 @@
+simple \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/simple/.osc/_project b/tests/update_fixtures/osctest/simple/.osc/_project
new file mode 100644
index 0000000..cea3bc8
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/_project
@@ -0,0 +1 @@
+osctest \ No newline at end of file
diff --git a/tests/update_fixtures/osctest/simple/.osc/foo b/tests/update_fixtures/osctest/simple/.osc/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/simple/.osc/merge b/tests/update_fixtures/osctest/simple/.osc/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/simple/.osc/nochange b/tests/update_fixtures/osctest/simple/.osc/nochange
new file mode 100644
index 0000000..0569b03
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/.osc/nochange
@@ -0,0 +1 @@
+This file didn't change.
diff --git a/tests/update_fixtures/osctest/simple/exists b/tests/update_fixtures/osctest/simple/exists
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/exists
diff --git a/tests/update_fixtures/osctest/simple/foo b/tests/update_fixtures/osctest/simple/foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/osctest/simple/merge b/tests/update_fixtures/osctest/simple/merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/osctest/simple/nochange b/tests/update_fixtures/osctest/simple/nochange
new file mode 100644
index 0000000..3a48a29
--- /dev/null
+++ b/tests/update_fixtures/osctest/simple/nochange
@@ -0,0 +1,2 @@
+This file didn't change but
+is modified.
diff --git a/tests/update_fixtures/testUpdateAlreadyInConflict_files b/tests/update_fixtures/testUpdateAlreadyInConflict_files
new file mode 100644
index 0000000..96b9752
--- /dev/null
+++ b/tests/update_fixtures/testUpdateAlreadyInConflict_files
@@ -0,0 +1,5 @@
+<directory name="already_in_conflict" rev="2" srcmd5="686b725018c89978678e15daa666ff85" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282133912" name="foo" size="23" />
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" mtime="1282134056" name="merge" size="7" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282133912" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateAlreadyInConflict_merge b/tests/update_fixtures/testUpdateAlreadyInConflict_merge
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/update_fixtures/testUpdateAlreadyInConflict_merge
@@ -0,0 +1 @@
+foobar
diff --git a/tests/update_fixtures/testUpdateConflict_files b/tests/update_fixtures/testUpdateConflict_files
new file mode 100644
index 0000000..93cd6a2
--- /dev/null
+++ b/tests/update_fixtures/testUpdateConflict_files
@@ -0,0 +1,5 @@
+<directory name="conflict" rev="2" srcmd5="6463d0bd161765e9a2b7186606c72ca1" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282130148" name="foo" size="23" />
+ <entry md5="89fcd308c6e6919c472e56ec82ace945" mtime="1282130545" name="merge" size="46" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282130148" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateConflict_merge b/tests/update_fixtures/testUpdateConflict_merge
new file mode 100644
index 0000000..f9f1e5a
--- /dev/null
+++ b/tests/update_fixtures/testUpdateConflict_merge
@@ -0,0 +1,4 @@
+Is
+it possible to
+merge this file?
+We'll see.
diff --git a/tests/update_fixtures/testUpdateDeletedFile_files b/tests/update_fixtures/testUpdateDeletedFile_files
new file mode 100644
index 0000000..9a8cc25
--- /dev/null
+++ b/tests/update_fixtures/testUpdateDeletedFile_files
@@ -0,0 +1,4 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLimitSizeAddDelete_exists b/tests/update_fixtures/testUpdateLimitSizeAddDelete_exists
new file mode 100644
index 0000000..ac79041
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLimitSizeAddDelete_exists
@@ -0,0 +1 @@
+small
diff --git a/tests/update_fixtures/testUpdateLimitSizeAddDelete_files b/tests/update_fixtures/testUpdateLimitSizeAddDelete_files
new file mode 100644
index 0000000..a06a209
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLimitSizeAddDelete_files
@@ -0,0 +1,6 @@
+<directory name="foo" rev="2" srcmd5="018a80019e08143e7ae324c778873d62" vrev="2">
+ <entry md5="ed955c917012307d982b7cdd5799ff1a" mtime="1282320398" name="bigfile" size="69" skipped="true" />
+ <entry md5="d15dbfcb847653913855e21370d83af1" mtime="1282553634" name="exists" size="6" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282320303" name="foo" size="23" skipped="true" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282320303" name="merge" size="48" skipped="true" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLimitSizeAddDelete_filesremote b/tests/update_fixtures/testUpdateLimitSizeAddDelete_filesremote
new file mode 100644
index 0000000..329f100
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLimitSizeAddDelete_filesremote
@@ -0,0 +1,6 @@
+<directory name="foo" rev="2" vrev="2" srcmd5="018a80019e08143e7ae324c778873d62">
+ <entry name="bigfile" md5="ed955c917012307d982b7cdd5799ff1a" size="69" mtime="1282320398" />
+ <entry name="exists" md5="d15dbfcb847653913855e21370d83af1" size="6" mtime="1282553634" />
+ <entry name="foo" md5="0d62ceea6020d75154078a20d8c9f9ba" size="23" mtime="1282320303" />
+ <entry name="merge" md5="17b9e9e1a032ed44e7a584dc6303ffa8" size="48" mtime="1282320303" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLimitSizeNoChange_files b/tests/update_fixtures/testUpdateLimitSizeNoChange_files
new file mode 100644
index 0000000..1745544
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLimitSizeNoChange_files
@@ -0,0 +1,6 @@
+<directory name="limitsize" rev="2" srcmd5="e51a3133d3d3eb2a48e06efb79e2d503" vrev="2">
+ <entry md5="ed955c917012307d982b7cdd5799ff1a" mtime="1282320398" name="bigfile" size="69" skipped="true" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282320303" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282320303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLimitSizeNoChange_filesremote b/tests/update_fixtures/testUpdateLimitSizeNoChange_filesremote
new file mode 100644
index 0000000..6a3ced8
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLimitSizeNoChange_filesremote
@@ -0,0 +1,6 @@
+<directory name="limitsize" rev="2" vrev="2" srcmd5="e51a3133d3d3eb2a48e06efb79e2d503">
+ <entry name="bigfile" md5="ed955c917012307d982b7cdd5799ff1a" size="69" mtime="1282320398" />
+ <entry name="foo" md5="0d62ceea6020d75154078a20d8c9f9ba" size="23" mtime="1282320303" />
+ <entry name="merge" md5="17b9e9e1a032ed44e7a584dc6303ffa8" size="48" mtime="1282320303" />
+ <entry name="nochange" md5="7efa70f68983fad1cf487f69dedf93e9" size="25" mtime="1282047303" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLocalDeletions_files b/tests/update_fixtures/testUpdateLocalDeletions_files
new file mode 100644
index 0000000..d1b7f80
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLocalDeletions_files
@@ -0,0 +1,5 @@
+<directory name="deleted" rev="2" srcmd5="0e717058d371ab9029336418c8c883bd" vrev="2">
+ <entry md5="2bb5f888a0063a0931c12f35851953e4" mtime="1282135005" name="foo" size="37" />
+ <entry md5="426e11f11438365322f102c02b0a33f0" mtime="1282134896" name="merge" size="50" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282134731" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLocalDeletions_foo b/tests/update_fixtures/testUpdateLocalDeletions_foo
new file mode 100644
index 0000000..0319af9
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLocalDeletions_foo
@@ -0,0 +1,2 @@
+This is a simple test.
+And an update
diff --git a/tests/update_fixtures/testUpdateLocalDeletions_merge b/tests/update_fixtures/testUpdateLocalDeletions_merge
new file mode 100644
index 0000000..df2934d
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLocalDeletions_merge
@@ -0,0 +1,4 @@
+Is
+it possible to
+merge this file?
+We'll see. Foo
diff --git a/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_files b/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_files
new file mode 100644
index 0000000..f03a9b5
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_files
@@ -0,0 +1,6 @@
+<directory name="limitsize_local" rev="2" srcmd5="e51a3133d3d3eb2a48e06efb79e2d503" vrev="2">
+ <entry md5="ed955c917012307d982b7cdd5799ff1a" mtime="1282320398" name="bigfile" size="69" skipped="true" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282320303" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282320303" name="merge" size="48" skipped="true" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_filesremote b/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_filesremote
new file mode 100644
index 0000000..4ffd780
--- /dev/null
+++ b/tests/update_fixtures/testUpdateLocalLimitSizeNoChange_filesremote
@@ -0,0 +1,6 @@
+<directory name="limitsize_local" rev="2" vrev="2" srcmd5="e51a3133d3d3eb2a48e06efb79e2d503">
+ <entry name="bigfile" md5="ed955c917012307d982b7cdd5799ff1a" size="69" mtime="1282320398" />
+ <entry name="foo" md5="0d62ceea6020d75154078a20d8c9f9ba" size="23" mtime="1282320303" />
+ <entry name="merge" md5="17b9e9e1a032ed44e7a584dc6303ffa8" size="48" mtime="1282320303" />
+ <entry name="nochange" md5="7efa70f68983fad1cf487f69dedf93e9" size="25" mtime="1282047303" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateMetaMode__meta b/tests/update_fixtures/testUpdateMetaMode__meta
new file mode 100644
index 0000000..2c2c701
--- /dev/null
+++ b/tests/update_fixtures/testUpdateMetaMode__meta
@@ -0,0 +1,4 @@
+<package project="osctest" name="metamode">
+ <title>foo</title>
+ <description />
+</package>
diff --git a/tests/update_fixtures/testUpdateMetaMode_filesremote b/tests/update_fixtures/testUpdateMetaMode_filesremote
new file mode 100644
index 0000000..faca442
--- /dev/null
+++ b/tests/update_fixtures/testUpdateMetaMode_filesremote
@@ -0,0 +1,3 @@
+<directory name="metamode" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="b995ef5586bdb37154bdeac0bda18c51" mtime="1283265642" name="_meta" size="95" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateNewFileLocalExists_exists b/tests/update_fixtures/testUpdateNewFileLocalExists_exists
new file mode 100644
index 0000000..9ca11f8
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNewFileLocalExists_exists
@@ -0,0 +1 @@
+exists
diff --git a/tests/update_fixtures/testUpdateNewFileLocalExists_files b/tests/update_fixtures/testUpdateNewFileLocalExists_files
new file mode 100644
index 0000000..543b47e
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNewFileLocalExists_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="28fe7af7e9985507cf51196fc67015b7" vrev="2">
+ <entry md5="7ba6ca74b292aaa5d46bc407ac5be166" mtime="1282060455" name="exists" size="7" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateNewFile_files b/tests/update_fixtures/testUpdateNewFile_files
new file mode 100644
index 0000000..f852ed2
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNewFile_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="9247f30cd5694f5301965a0f20a2ed16" vrev="2">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282054323" name="upstream_added" size="23" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateNewFile_upstream_added b/tests/update_fixtures/testUpdateNewFile_upstream_added
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNewFile_upstream_added
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/testUpdateNew_filesremote b/tests/update_fixtures/testUpdateNew_filesremote
new file mode 100644
index 0000000..432daa9
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNew_filesremote
@@ -0,0 +1,2 @@
+<directory name="new">
+</directory>
diff --git a/tests/update_fixtures/testUpdateNoChanges_files b/tests/update_fixtures/testUpdateNoChanges_files
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/update_fixtures/testUpdateNoChanges_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateRestore_files b/tests/update_fixtures/testUpdateRestore_files
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/update_fixtures/testUpdateRestore_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateRestore_foo b/tests/update_fixtures/testUpdateRestore_foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/testUpdateRestore_foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/testUpdateResumeDeletedFile_files b/tests/update_fixtures/testUpdateResumeDeletedFile_files
new file mode 100644
index 0000000..d2e3da5
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResumeDeletedFile_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="1" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="1">
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282047302" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateResumeDeletedFile_foo b/tests/update_fixtures/testUpdateResumeDeletedFile_foo
new file mode 100644
index 0000000..3bb34cf
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResumeDeletedFile_foo
@@ -0,0 +1 @@
+This is a simple test.
diff --git a/tests/update_fixtures/testUpdateResumeDeletedFile_merge b/tests/update_fixtures/testUpdateResumeDeletedFile_merge
new file mode 100644
index 0000000..0b4685d
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResumeDeletedFile_merge
@@ -0,0 +1,4 @@
+Is it
+possible to
+merge this file?
+I hope so...
diff --git a/tests/update_fixtures/testUpdateResume_files b/tests/update_fixtures/testUpdateResume_files
new file mode 100644
index 0000000..0b0a0c8
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResume_files
@@ -0,0 +1,6 @@
+<directory name="simple" rev="2" srcmd5="3ac41c59a5ed169d5ffef4d824700f7d" vrev="2">
+ <entry md5="ff22941336956098ae9a564289d1bf1b" mtime="1282137256" name="added" size="15" />
+ <entry md5="14758f1afd44c09b7992073ccf00b43d" mtime="1282137220" name="foo" size="7" />
+ <entry md5="256d8f76ba7a0a231fb46a84866f25d8" mtime="1282137238" name="merge" size="20" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateResume_foo b/tests/update_fixtures/testUpdateResume_foo
new file mode 100644
index 0000000..323fae0
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResume_foo
@@ -0,0 +1 @@
+foobar
diff --git a/tests/update_fixtures/testUpdateResume_merge b/tests/update_fixtures/testUpdateResume_merge
new file mode 100644
index 0000000..2563d89
--- /dev/null
+++ b/tests/update_fixtures/testUpdateResume_merge
@@ -0,0 +1,5 @@
+xxx
+xxx
+yyy
+zzz
+zzz
diff --git a/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:bar b/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:bar
new file mode 100644
index 0000000..5fe9f1f
--- /dev/null
+++ b/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:bar
@@ -0,0 +1 @@
+another service
diff --git a/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:foo b/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:foo
new file mode 100644
index 0000000..ac79041
--- /dev/null
+++ b/tests/update_fixtures/testUpdateServiceFilesAddDelete__service:foo
@@ -0,0 +1 @@
+small
diff --git a/tests/update_fixtures/testUpdateServiceFilesAddDelete_bigfile b/tests/update_fixtures/testUpdateServiceFilesAddDelete_bigfile
new file mode 100644
index 0000000..8b7b0f9
--- /dev/null
+++ b/tests/update_fixtures/testUpdateServiceFilesAddDelete_bigfile
@@ -0,0 +1,5 @@
+This is a file
+with a lot of
+text. Foo foo
+bar bar bar.
+foobarfoobar
diff --git a/tests/update_fixtures/testUpdateServiceFilesAddDelete_files b/tests/update_fixtures/testUpdateServiceFilesAddDelete_files
new file mode 100644
index 0000000..35e0945
--- /dev/null
+++ b/tests/update_fixtures/testUpdateServiceFilesAddDelete_files
@@ -0,0 +1,7 @@
+<directory name="foo" rev="2" srcmd5="1c5d541a029694c43d5341cabcb4f40f" vrev="2">
+ <entry md5="a0106bad78c9070662d5cde42ee35f23" mtime="1282564656" name="_service:bar" size="16" skipped="true" />
+ <entry md5="d15dbfcb847653913855e21370d83af1" mtime="1282561867" name="_service:foo" size="6" skipped="true" />
+ <entry md5="ed955c917012307d982b7cdd5799ff1a" mtime="1282320398" name="bigfile" size="69" />
+ <entry md5="0d62ceea6020d75154078a20d8c9f9ba" mtime="1282320303" name="foo" size="23" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282320303" name="merge" size="48" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateServiceFilesAddDelete_filesremote b/tests/update_fixtures/testUpdateServiceFilesAddDelete_filesremote
new file mode 100644
index 0000000..8f4e3ae
--- /dev/null
+++ b/tests/update_fixtures/testUpdateServiceFilesAddDelete_filesremote
@@ -0,0 +1,7 @@
+<directory name="foo" rev="2" vrev="2" srcmd5="1c5d541a029694c43d5341cabcb4f40f">
+ <entry name="_service:bar" md5="a0106bad78c9070662d5cde42ee35f23" size="16" mtime="1282564656" />
+ <entry name="_service:foo" md5="d15dbfcb847653913855e21370d83af1" size="6" mtime="1282561867" />
+ <entry name="bigfile" md5="ed955c917012307d982b7cdd5799ff1a" size="69" mtime="1282320398" />
+ <entry name="foo" md5="0d62ceea6020d75154078a20d8c9f9ba" size="23" mtime="1282320303" />
+ <entry name="merge" md5="17b9e9e1a032ed44e7a584dc6303ffa8" size="48" mtime="1282320303" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateUpstreamModifiedFile_files b/tests/update_fixtures/testUpdateUpstreamModifiedFile_files
new file mode 100644
index 0000000..c605722
--- /dev/null
+++ b/tests/update_fixtures/testUpdateUpstreamModifiedFile_files
@@ -0,0 +1,5 @@
+<directory name="simple" rev="2" srcmd5="2df1eacfe03a3bec2112529e7f4dc39a" vrev="2">
+ <entry md5="bb3a1efda68dff80ec3a2fb599b97ad8" mtime="1282058167" name="foo" size="39" />
+ <entry md5="17b9e9e1a032ed44e7a584dc6303ffa8" mtime="1282047303" name="merge" size="48" />
+ <entry md5="7efa70f68983fad1cf487f69dedf93e9" mtime="1282047303" name="nochange" size="25" />
+</directory>
diff --git a/tests/update_fixtures/testUpdateUpstreamModifiedFile_foo b/tests/update_fixtures/testUpdateUpstreamModifiedFile_foo
new file mode 100644
index 0000000..4083ca8
--- /dev/null
+++ b/tests/update_fixtures/testUpdateUpstreamModifiedFile_foo
@@ -0,0 +1,3 @@
+<added>
+This is a simple test.
+<added>