File obs-scm-bridge-0.2.obscpio of Package obs-scm-bridge.24636

07070100000000000081A400000000000000000000000162A070E500000179000000000000000000000000000000000000001C00000000obs-scm-bridge-0.2/Makefileprefix = /usr
PYTHON ?= python

servicedir = ${prefix}/lib/obs/service

all:

install:
	install -d $(DESTDIR)$(servicedir)
	install -m 0755 obs_scm_bridge $(DESTDIR)$(servicedir)

test:
	flake8 set_version tests/
	${PYTHON} -m unittest discover tests/

clean:
	find -name "*.pyc" -exec rm {} \;
	find -name '*.pyo' -exec rm {} \;
	rm -rf set_versionc

.PHONY: all install test
07070100000001000081A400000000000000000000000162A070E500000820000000000000000000000000000000000000001D00000000obs-scm-bridge-0.2/README.md
Native OBS SCM bridge helper
============================

Native OBS scm support for the build recipies and additional files. This is bridging an external authorative
scm repository into OBS. Any source change or merge workflow must be provided via the scm repository 
hoster in this scenario.

Only git is supported atm, but this can be extended later to further systems.

It is not recommended to put large binary files into git directly as this won't scale. Use the
asset support instead, which is described in pbuild documentation:

  http://opensuse.github.io/obs-build/pbuild.html#_remote_assets

These assets will be downloaded by osc and OBS. The verification via sha256 sum is optional.

HOWTO manage a single package
=============================

The current way to define a git repository for an OBS package is using the `scmsync`
element inside the package meta.

```
<scmsync>https://github.com/foo/bar</scmsync>
```

For doing a local checkout use the currently experimental osc from

  https://download.opensuse.org/repositories/home:/adrianSuSE:/OBSGIT/

This version allows you to do

# osc co $project <$package>

which will create a git repository inside of the classic osc checkout.

The only further tested functionality is to do local builds atm.

HOWTO manage an entire project
==============================

A git repository can also get defined for entire project. This can be done
via the scmsync element in project meta.

Any top level subdirectory will be handled as package container. 

It is recomended to use git submodules for each package if it is a larger
project. This allows partial cloning of the specific package.

TODO
====

 * Monitoring changes in referenced repository. (can currently be workarounded
   via "osc service rr")

 * osc upstream integration

 * signature validation

 * find a better way to store files in .osc and .assets of the checkout, as
   they do not belong to the git repository
    auto extending .gitignore? (esp. when downloading asset files?)

 * make cpio generation bit identical (avoid mtime from clone)

07070100000002000081A400000000000000000000000162A070E500000506000000000000000000000000000000000000002700000000obs-scm-bridge-0.2/obs-scm-bridge.spec#
# spec file
#
# Copyright (c) 2021 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.

# Please submit bugfixes or comments via https://bugs.opensuse.org/
#

%if 0%{?fedora} || 0%{?rhel}
%define build_pkg_name obs-build
%else
%define build_pkg_name build
%endif

Name:           obs-scm-bridge
Version:        0.0.1
Release:        0
Summary:        A help service to work with git repositories in OBS
License:        GPL-2.0-or-later
URL:            https://github.com/openSUSE/obs-scm-bridge
Source0:        %{name}-%{version}.tar.xz
Requires:       %{build_pkg_name} >= 20211125
BuildArch:      noarch
Recommends:     python3-packaging


%description

%prep
%autosetup

%build

%install
make DESTDIR=%{buildroot} install

%files
%{_prefix}/lib/obs/service

%changelog
07070100000003000081ED00000000000000000000000162A070E500003868000000000000000000000000000000000000002200000000obs-scm-bridge-0.2/obs_scm_bridge#!/usr/bin/python3
# -*- coding: utf-8 -*-

# scm (only git atm) cloning and packaging for Open Build Service
# 
# (C) 2021 by Adrian Schröter <adrian@suse.de>
#
# 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.
# See http://www.gnu.org/licenses/gpl-2.0.html for full license text.

import argparse
import os
import re
import shutil
import sys
import logging
import subprocess
import tempfile
from html import escape
import urllib.parse
import configparser

outdir = None
download_assets = '/usr/lib/build/download_assets'
export_debian_orig_from_git = '/usr/lib/build/export_debian_orig_from_git'
pack_directories = False
get_assets = False
shallow_clone = True

if os.environ.get('DEBUG_SCM_BRIDGE') == "1":
    logging.getLogger().setLevel(logging.DEBUG)
if os.environ.get('OBS_SERVICE_DAEMON'):
    pack_directories = True
    get_assets = True
if os.environ.get('OSC_VERSION'):
    get_assets = True
    shallow_clone = False
os.environ['LANG'] = "C"

class ObsGit(object):
    def __init__(self, outdir, url):
        self.outdir   = outdir
        self.revision = None
        self.subdir   = None
        self.lfs = False
        self.arch = []
        self.url = list(urllib.parse.urlparse(url))
        query = urllib.parse.parse_qs(self.url[4]);
        if "subdir" in query:
            self.subdir = query['subdir'][0]
            del query['subdir']
            self.url[4] = urllib.parse.urlencode(query)
        if "arch" in query:
            self.arch = query['arch']
            del query['arch']
            self.url[4] = urllib.parse.urlencode(query)
        if "lfs" in query:
            self.lfs = True
            del query['lfs']
            self.url[4] = urllib.parse.urlencode(query)
        if self.url[5]:
            self.revision = self.url[5]
            self.url[5] = ''

    def run_cmd(self, cmd, cwd=None, stdout=subprocess.PIPE, fatal=None):
        logging.debug("COMMAND: %s" % cmd)
        stderr = subprocess.PIPE
        if stdout == subprocess.PIPE:
            stderr = subprocess.STDOUT
        proc = subprocess.Popen(cmd,
                                shell=False,
                                stdout=stdout,
                                stderr=stderr,
                                cwd=cwd)
        output = proc.communicate()[0]
        if isinstance(output, bytes):
            output = output.decode('UTF-8')
        logging.debug("RESULT(%d): %s", proc.returncode, repr(output))
        if fatal and proc.returncode != 0:
            print("ERROR: " + fatal + " failed: ", output)
            sys.exit(proc.returncode)
        return (proc.returncode, output)

    def do_lfs(self, outdir):
        cmd = [ 'git', '-C', outdir, 'lfs', 'fetch' ]
        self.run_cmd(cmd, fatal="git lfs fetch")

    def do_clone_commit(self, outdir):
        cmd = [ 'git', 'init', outdir ]
        self.run_cmd(cmd, fatal="git init")
        cmd = [ 'git', '-C', outdir, 'remote', 'add', 'origin', urllib.parse.urlunparse(self.url) ]
        self.run_cmd(cmd, fatal="git remote add origin")
        cmd = [ 'git', '-C', outdir, 'fetch', 'origin', self.revision ]
        if shallow_clone:
            cmd += [ '--depth', '1' ]
        print(cmd)
        self.run_cmd(cmd, fatal="git fetch")
        cmd = [ 'git', '-C', outdir, 'checkout', '-q', self.revision ]
        self.run_cmd(cmd, fatal="git checkout")

    def do_clone(self, outdir):
        if self.revision and re.match(r"^[0-9a-fA-F]{40,}$", self.revision):
            self.do_clone_commit(outdir)
            if self.lfs:
                self.do_lfs(outdir)
            return
        cmd = [ 'git', 'clone', urllib.parse.urlunparse(self.url), outdir ]
        if shallow_clone:
            cmd += [ '--depth', '1' ]
        if self.revision:
            cmd.insert(2, '-b')
            cmd.insert(3, self.revision)
        self.run_cmd(cmd, fatal="git clone")
        if self.lfs:
            self.do_lfs(outdir)

    def clone(self):
        if not self.subdir:
            self.do_clone(self.outdir)
            return
        clonedir = tempfile.mkdtemp(prefix="obs-scm-bridge")
        self.do_clone(clonedir)
        fromdir = os.path.join(clonedir, self.subdir)
        if not os.path.realpath(fromdir+'/').startswith(os.path.realpath(clonedir+'/')):
            print("ERROR: subdir is not below clone directory")
            sys.exit(1)
        if not os.path.isdir(fromdir):
            print("ERROR: subdir " + self.subdir + " does not exist")
            sys.exit(1)
        if not os.path.isdir(self.outdir):
            os.makedirs(self.outdir)
        for name in os.listdir(fromdir):
            shutil.move(os.path.join(fromdir, name), self.outdir)
        shutil.rmtree(clonedir)

    def fetch_tags(self):
        cmd = [ 'git', '-C', self.outdir, 'fetch', '--tags', 'origin', '+refs/heads/*:refs/remotes/origin/*' ]
        logging.info("fetching all tags")
        self.run_cmd(cmd, fatal="fetch --tags")

    def cpio_directory(self, directory):
        logging.info("create archivefile for %s", directory)
        cmd = [ download_assets, '--create-cpio', '--', directory ]
        archivefile = open(directory + '.obscpio', 'w')
        self.run_cmd(cmd, stdout=archivefile, fatal="cpio creation")
        archivefile.close()

    def cpio_specials(self, specials):
        if not specials:
            return
        logging.info("create archivefile for specials")
        cmd = [ download_assets, '--create-cpio', '--', '.' ] + specials
        archivefile = open('build.specials.obscpio', 'w')
        self.run_cmd(cmd, stdout=archivefile, fatal="cpio creation")
        archivefile.close()

    def cpio_directories(self):
        logging.debug("walk via %s", self.outdir)
        os.chdir(self.outdir)
        listing = sorted(os.listdir("."))
        specials = []
        for name in listing:
            if name == '.git':
                # we do not store git meta data service side atm to avoid bloat storage
                # however, this will break some builds, so we will need an opt-out in future
                shutil.rmtree(name)
                continue
            if name[0:1] == '.':
                specials.append(name)
                continue
            if os.path.islink(name):
                specials.append(name)
                continue
            if os.path.isdir(name):
                logging.info("CPIO %s ", name)
                self.cpio_directory(name)
                shutil.rmtree(name)
        if specials:
            self.cpio_specials(specials)
            for name in specials:
                if os.path.isdir(name):
                    shutil.rmtree(name)
                else:
                    os.unlink(name)

    def get_assets(self):
        logging.info("downloading assets")
        cmd = [ download_assets ]
        for arch in self.arch:
            cmd += [ '--arch', arch ]
        if pack_directories:
            cmd += [ '--noassetdir', '--', self.outdir ]
        else:
            cmd += [ '--unpack', '--noassetdir', '--', self.outdir ]
        self.run_cmd(cmd, fatal="asset download")

    def copyfile(self, src, dst):
        cmd = [ 'cp', '-af', self.outdir + "/" + src, self.outdir + "/" + dst ]
        self.run_cmd(cmd, fatal="file copy")

    def export_debian_files(self):
        if os.path.isfile(self.outdir + "/debian/control") and \
                not os.path.isfile(self.outdir + "/debian.control"):
            self.copyfile("debian/control", "debian.control")
        if os.path.isfile(self.outdir + "/debian/changelog") and \
                not os.path.isfile(self.outdir + "/debian.changelog"):
            self.copyfile("debian/changelog", "debian.changelog")

    def get_debian_origtar(self):
        if os.path.isfile(self.outdir + "/debian/control"):
            # need up get all tags 
            if not self.subdir:
                self.fetch_tags()
            cmd = [ export_debian_orig_from_git, self.outdir ]
            logging.info("exporting debian origtar")
            self.run_cmd(cmd, fatal="debian origtar export")

    def get_subdir_info(self, dir):
        cmd = [ download_assets, '--show-dir-srcmd5', '--', dir ]
        rcode, info = self.run_cmd(cmd, fatal="download_assets --show-dir-srcmd5")
        return info.strip()

    def write_info_file(self, filename, info):
        infofile = open(filename, 'w')
        infofile.write(info + '\n')
        infofile.close()

    def add_service_info(self):
        info = None
        if self.subdir:
            info = self.get_subdir_info(self.outdir)
        else:
            cmd = [ 'git', '-C', outdir, 'show', '-s', '--pretty=format:%H', 'HEAD' ]
            rcode, info = self.run_cmd(cmd, fatal="git show -s HEAD")
            info = info.strip()
        if info:
            self.write_info_file(os.path.join(self.outdir, "_service_info"), info)

    def write_package_xml_file(self, name, url):
        xmlfile = open(name + '.xml', 'w')
        xmlfile.write('<package name="' + escape(name) + '">\n')
        xmlfile.write('  <scmsync>' + escape(url) + '</scmsync>\n')
        xmlfile.write('</package>\n')
        xmlfile.close()

    def list_submodule_revisions(self):
        revisions = {}
        cmd = [ 'git', 'ls-tree', 'HEAD', '.' ]
        rcode, output = self.run_cmd(cmd, fatal="git ls-tree")
        for line in output.splitlines():
            lstree = line.split(maxsplit=4)
            if lstree[1] == 'commit' and len(lstree[2]) >= 40:
                revisions[lstree[3]] = lstree[2]
        return revisions
           
    def generate_package_xml_files(self):
        logging.debug("walk via %s", self.outdir)
        os.chdir(self.outdir)
        export_files = set(["_config"])

        # find all top level git submodules
        gitsubmodules = set()
        if os.path.isfile('.gitmodules'):
            gsmconfig = configparser.ConfigParser()
            gsmconfig.read('.gitmodules')
            revisions = None
            for section in gsmconfig.sections():
                if not 'path' in gsmconfig[section]:
                    logging.warn("path not defined for git submodule " + section)
                    continue
                if not 'url' in gsmconfig[section]:
                    logging.warn("url not defined for git submodule " + section)
                    continue
                path = gsmconfig[section]['path']
                url = gsmconfig[section]['url']

                if '/' in path:
                    # we handle only top level submodules in project mode
                    continue

                # find revision of submodule
                if revisions is None:
                    revisions = self.list_submodule_revisions()
                revision = revisions.get(path, None)
                if not revision:
                    logging.error("Could not determine revision of submodule " + section)
                    sys.exit(1)

                # all good, write xml file and register the module
                gitsubmodules.add(path)
                url = list(urllib.parse.urlparse(url))
                url[5] = revision
                if self.arch:
                    query = urllib.parse.parse_qs(url[4]);
                    query['arch'] = self.arch
                    url[4] = urllib.parse.urlencode(query)
                self.write_package_xml_file(path, urllib.parse.urlunparse(url))
                self.write_info_file(path + ".info", revision)
                export_files.add(path + ".xml")
                export_files.add(path + ".info")
                shutil.rmtree(path)

        # handle plain files and directories
        listing = sorted(os.listdir("."))
        regexp =  re.compile(r"^[a-zA-Z0-9\.\-\_\+]*$");
        for name in listing:
            if name == '.git':
                shutil.rmtree(name)
                continue
            if os.path.isdir(name):
                if name in gitsubmodules:
                    # already handled as git submodule
                    continue

                info = self.get_subdir_info(name)
                shutil.rmtree(name)
                if not regexp.match(name):
                    logging.warn("directory name contains invalid char: " + name)
                    continue

                # add subdir info file
                self.write_info_file(name + ".info", info)

                # add subdir parameter to url
                url = self.url
                query = urllib.parse.parse_qs(url[4])
                query['subdir'] = name
                url[4] = urllib.parse.urlencode(query)

                self.write_package_xml_file(name, urllib.parse.urlunparse(url))
            else:
                if not name in export_files:
                    os.unlink(name)

if __name__ == '__main__':

    parser = argparse.ArgumentParser(
        description='Open Build Service source service for managing packaging files in git.'
        'This is a special service for OBS git integration.')
    parser.add_argument('--outdir', required=True,
                        help='output directory for modified sources')
    parser.add_argument('--url',
                        help='REQUIRED: url to git repository')
    parser.add_argument('--projectmode',
                        help='just return the package list based on the subdirectories')
    parser.add_argument('--debug',
                        help='verbose debug mode')
    args = vars(parser.parse_args())

    url = args['url']
    outdir = args['outdir']
    project_mode = args['projectmode']

    if not outdir:
        print("no outdir specified")
        sys.exit(-1)

    if not url:
        print("no url specified")
        sys.exit(-1)

    if args['debug']:
        logging.getLogger().setLevel(logging.DEBUG)
        logging.debug("Running in debug mode")

    # workflow
    obsgit = ObsGit(outdir, url)
    obsgit.clone()
    if project_mode == 'true' or project_mode == '1':
        obsgit.generate_package_xml_files()
        sys.exit(0)

    if pack_directories:
        obsgit.add_service_info()
    if get_assets:
        obsgit.get_assets()
        obsgit.get_debian_origtar()
    if pack_directories:
        obsgit.export_debian_files()
        obsgit.cpio_directories()

07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!37 blocks
openSUSE Build Service is sponsored by