Skip to content

Instantly share code, notes, and snippets.

@jberry-suse
Created January 25, 2017 03:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jberry-suse/c0106957fac0b64453964ee96de786df to your computer and use it in GitHub Desktop.
Save jberry-suse/c0106957fac0b64453964ee96de786df to your computer and use it in GitHub Desktop.
diff --git a/osc-staging.py b/osc-staging.py
index 980f236..4f7dbdd 100644
--- a/osc-staging.py
+++ b/osc-staging.py
@@ -100,8 +100,8 @@ def _full_project_name(self, project):
@cmdln.option('--wipe-cache', dest='wipe_cache', action='store_true', default=False,
help='wipe GET request cache before executing')
@cmdln.option('-m', '--message', help='message used by ignore command')
-@cmdln.option('--filter-by', help='xpath by which to filter requests')
-@cmdln.option('--group-by', help='xpath by which to group requests')
+@cmdln.option('--filter-by', action='append', help='xpath by which to filter requests')
+@cmdln.option('--group-by', action='append', help='xpath by which to group requests')
@cmdln.option('-i', '--interactive', action='store_true', help='interactively modify selection proposal')
def do_staging(self, subcmd, opts, *args):
"""${cmd_name}: Commands to work with staging projects
@@ -133,6 +133,13 @@ def do_staging(self, subcmd, opts, *args):
"list" will pick the requests not in rings
"select" will add requests to the project
+ Stagings are expected to be either in short-hand or the full project
+ name. For example letter or named stagings can be specified simply as
+ A, B, Gcc6, etc, while adi stagings can be specified as adi:1, adi:2,
+ etc. Currently, adi stagings are not supported in proposal mode.
+
+ Requests may either be the target package or the request ID.
+
When using --filter-by or --group-by the xpath will be applied to the
request node as returned by OBS. Several values will supplement the
normal request node.
@@ -146,13 +153,16 @@ def do_staging(self, subcmd, opts, *args):
--filter-by './action/target[starts-with(@package, "yast-"]'
--filter-by './action/source/[@devel_project="YaST:Head"]'
--filter-by './action/target[@ring="1-MinimalX"]'
+ --filter-by '@id!="1234567"'
--group-by='./action/source/@devel_project'
--group-by='./action/target/@ring'
- Note that when using proposal mode multiple letter stagings to consider
- may be provided in addition to a list of request IDs by which to filter.
- A more complex example:
+ Multiple filter-by or group-by options may be used at the same time.
+
+ Note that when using proposal mode, multiple stagings to consider may be
+ provided in addition to a list of requests by which to filter. A more
+ complex example:
select --group-by='./action/source/@devel_project' A B C 123 456 789
@@ -182,8 +192,8 @@ def do_staging(self, subcmd, opts, *args):
osc staging ignore [-m MESSAGE] REQUEST...
osc staging unignore REQUEST...|all
osc staging list [--supersede]
- osc staging select [--no-freeze] [--move [--from PROJECT] LETTER REQUEST...
- osc staging select [--no-freeze] [[--interactive] [--filter-by] [--group-by]] [LETTER...] [REQUEST...]
+ osc staging select [--no-freeze] [--move [--from PROJECT] STAGING REQUEST...
+ osc staging select [--no-freeze] [[--interactive] [--filter-by...] [--group-by...]] [STAGING...] [REQUEST...]
osc staging unselect REQUEST...
osc staging repair REQUEST...
"""
@@ -280,30 +290,45 @@ def do_staging(self, subcmd, opts, *args):
elif cmd == 'unselect':
UnselectCommand(api).perform(args[1:])
elif cmd == 'select':
+ # Include list of all stagings in short-hand and by full name.
+ existing_stagings = api.get_staging_projects_short(None)
+ existing_stagings += [p for p in api.get_staging_projects() if not p.endswith(':DVD')]
stagings = []
- request_ids = []
+ requests = []
for arg in args[1:]:
- if not arg.isdigit():
- stagings.append(arg)
- else:
- request_ids.append(arg)
-
- if len(stagings) != 1 or len(request_ids) == 0 or opts.filter_by or opts.group_by:
+ # Since requests may be given by either request ID or package
+ # name and stagings may include multi-letter special stagings
+ # there is no easy way to distinguish between stagings and
+ # requests in arguments. Therefore, check if argument is in the
+ # list of short-hand and full project name stagings, otherwise
+ # consider it a request. This also allows for special stagings
+ # with the same name as package, but the staging will be assumed
+ # first time around. The current practice seems to be to start a
+ # special staging with a capital letter which makes them unique.
+ # lastly adi stagings are consistently prefix with adi: which
+ # also makes it consistent to distinguish them from request IDs.
+ if arg in existing_stagings and arg not in stagings:
+ stagings.append(api.extract_staging_short(arg))
+ elif arg not in requests:
+ requests.append(arg)
+
+ if len(stagings) != 1 or len(requests) == 0 or opts.filter_by or opts.group_by:
if opts.move or opts.from_:
print('--move and --from must be used with explicit staging and request list')
return
- requests = api.get_open_requests()
- splitter = RequestSplitter(api, requests, in_ring=True)
- if len(request_ids) > 0:
- splitter.filter_add_ids(request_ids)
- else:
+ splitter = RequestSplitter(api, api.get_open_requests(), in_ring=True)
+ if len(requests) > 0:
+ splitter.filter_add_requests(requests)
+ if len(splitter.filters) == 0:
splitter.filter_add('./action[not(@type="add_role" or @type="change_devel")]')
splitter.filter_add('@ignored="false"')
if opts.filter_by:
- splitter.filter_add(opts.filter_by)
+ for filter_by in opts.filter_by:
+ splitter.filter_add(filter_by)
if opts.group_by:
- splitter.group_by(opts.group_by)
+ for group_by in opts.group_by:
+ splitter.group_by(group_by)
splitter.split()
result = splitter.propose_assignment(stagings)
@@ -317,14 +342,14 @@ def do_staging(self, subcmd, opts, *args):
if opts.interactive:
with tempfile.NamedTemporaryFile(suffix='.yml') as temp:
- temp.write('# staging proposal\n')
- temp.write('# make modifications or comment/remove lines\n\n')
- temp.write(yaml.dump(splitter.proposal, default_flow_style=False) + '\n\n')
- before = ', '.join(sorted(splitter._stagings_available.keys()))
- after = ', '.join(sorted(splitter.stagings_available.keys()))
- temp.write('# stagings available\n')
- temp.write('# - before: {}\n'.format(before))
- temp.write('# - after: {}\n'.format(after))
+ temp.write(yaml.safe_dump(splitter.proposal, default_flow_style=False) + '\n\n')
+ temp.write('# move requests between stagings or comment/remove them\n')
+ temp.write('# change the target staging for a group\n')
+ temp.write('# stagings\n')
+ temp.write('# - considered: {}\n'
+ .format(', '.join(sorted(splitter.stagings_considerable.keys()))))
+ temp.write('# - available: {}\n'
+ .format(', '.join(sorted(splitter.stagings_available.keys()))))
temp.flush()
editor = os.getenv('EDITOR')
@@ -334,10 +359,11 @@ def do_staging(self, subcmd, opts, *args):
proposal = yaml.safe_load(open(temp.name).read())
- print(yaml.dump(proposal, default_flow_style=False))
+ print(yaml.safe_dump(proposal, default_flow_style=False))
- print('Accept proposal? [y/n]: ', end='')
- if raw_input().lower() != 'y':
+ print('Accept proposal? [y/n] (y): ', end='')
+ response = raw_input().lower()
+ if response != '' and response != 'y':
print('Quit')
return
@@ -351,17 +377,17 @@ def do_staging(self, subcmd, opts, *args):
# SelectCommand expects strings.
request_ids = map(str, g['requests'].keys())
- target_project = api.prj_from_letter(g['staging'])
+ target_project = api.prj_from_short(g['staging'])
SelectCommand(api, target_project) \
.perform(request_ids, opts.move, opts.from_, opts.no_freeze)
else:
- target_project = api.prj_from_letter(stagings[0])
+ target_project = api.prj_from_short(stagings[0])
if opts.add:
api.mark_additional_packages(target_project, [opts.add])
else:
SelectCommand(api, target_project) \
- .perform(args[2:], opts.move, opts.from_, opts.no_freeze)
+ .perform(requests, opts.move, opts.from_, opts.no_freeze)
elif cmd == 'cleanup_rings':
CleanupRings(api).perform()
elif cmd == 'ignore':
diff --git a/osclib/list_command.py b/osclib/list_command.py
index aabb4b0..78a5d7d 100644
--- a/osclib/list_command.py
+++ b/osclib/list_command.py
@@ -29,7 +29,6 @@ class ListCommand:
# First dispatch all possible requests
self.api.dispatch_open_requests()
- # Print out the left overs
requests = self.api.get_open_requests()
requests_ignored = self.api.get_ignored_requests()
diff --git a/osclib/request_splitter.py b/osclib/request_splitter.py
index 14e4b9a..ec644ad 100644
--- a/osclib/request_splitter.py
+++ b/osclib/request_splitter.py
@@ -23,9 +23,11 @@ class RequestSplitter(object):
def filter_add(self, xpath):
self.filters.append(ET.XPath(xpath))
- def filter_add_ids(self, request_ids):
- request_ids = ' ' + ' '.join(request_ids) + ' '
- self.filter_add('contains("{}", concat(" ", @id, " "))'.format(request_ids))
+ def filter_add_requests(self, requests):
+ requests = ' ' + ' '.join(requests) + ' '
+ self.filter_add('contains("{requests}", concat(" ", @id, " ")) or '
+ 'contains("{requests}", concat(" ", ./action/target/@package, " "))'
+ .format(requests=requests))
def group_by(self, xpath):
self.groups.append(ET.XPath(xpath))
@@ -33,6 +35,8 @@ class RequestSplitter(object):
def filter_only(self):
ret = []
for request in self.requests:
+ target_package = request.find('./action/target').get('package')
+ self.suppliment(request, target_package)
if self.filter_check(request):
ret.append(request)
return ret
@@ -111,45 +115,43 @@ class RequestSplitter(object):
key.append(element[0])
return '__'.join(key)
- def propose_staging_load(self):
- self._stagings_available = {}
+ def propose_stagings_load(self, stagings):
+ self.stagings_considerable = {}
if self.api.rings:
xpath = 'link[@project="{}"]'.format(self.api.rings[0])
- for staging in self.api.get_staging_projects():
- if self.api.is_adi_project(staging) or staging.endswith(':DVD'):
- continue
- # TODO Allow stagings that have not finished building by threshold.
- if len(self.api.get_prj_pseudometa(staging)['requests']) > 0:
- continue
+ # Use specified list of stagings, otherwise only empty, letter stagings.
+ if len(stagings) == 0:
+ stagings = self.api.get_staging_projects_short()
+ filter_skip = False
+ else:
+ filter_skip = True
- letter = self.api.extract_staging_letter(staging)
- if len(letter) > 1:
- continue
+ for staging in stagings:
+ project = self.api.prj_from_short(staging)
+
+ if not filter_skip:
+ if len(staging) > 1:
+ continue
+
+ # TODO Allow stagings that have not finished building by threshold.
+ if len(self.api.get_prj_pseudometa(project)['requests']) > 0:
+ continue
if self.api.rings:
- meta = self.api.get_prj_meta(staging)
- self._stagings_available[letter] = True if meta.find(xpath) is not None else False
+ # Determine if staging is bootstrapped.
+ meta = self.api.get_prj_meta(project)
+ self.stagings_considerable[staging] = True if meta.find(xpath) is not None else False
else:
- self._stagings_available[letter] = False
-
- def propose_staging_list(self, stagings):
- if not hasattr(self, '_stagings_available'):
- self.propose_staging_load()
-
- if len(stagings) > 0:
- self.stagings_available = {}
- # Filter those not in the list of stagings to consider.
- for staging, bootstrapped in self._stagings_available.items():
- if staging in stagings:
- self.stagings_available[staging] = bootstrapped
- else:
- self.stagings_available = self._stagings_available.copy()
+ self.stagings_considerable[staging] = False
+
+ # Allow both considered and remaining to be accessible after proposal.
+ self.stagings_available = self.stagings_considerable.copy()
def propose_assignment(self, stagings):
- # Determine available stagings and make working copy
- self.propose_staging_list(stagings)
+ # Determine available stagings and make working copy.
+ self.propose_stagings_load(stagings)
if len(self.grouped) > len(self.stagings_available):
return 'more groups than available stagings'
diff --git a/osclib/stagingapi.py b/osclib/stagingapi.py
index 8086d56..d97648f 100644
--- a/osclib/stagingapi.py
+++ b/osclib/stagingapi.py
@@ -314,13 +314,36 @@ class StagingAPI(object):
projects.append(val.get('name'))
return projects
- def is_adi_project(self, p):
- return ':adi:' in p
-
- def extract_staging_letter(self, p):
+ def extract_staging_short(self, p):
+ if not ':' in p:
+ return p
+ prefix = len(self.cstaging) + 1
if p.endswith(':DVD'):
p = p[:-4]
- return p.split(':')[-1]
+ return p[prefix:]
+
+ def prj_from_short(self, name):
+ if name.startswith(self.cstaging):
+ return name
+ return '{}:{}'.format(self.cstaging, name)
+
+ def get_staging_projects_short(self, adi=False):
+ """
+ Get list of staging project by short-hand names.
+ :param adi: True for only adi stagings, False for only non-adi stagings,
+ and None for both.
+ """
+ prefix = len(self.cstaging) + 1
+ projects = []
+ for project in self.get_staging_projects():
+ if project.endswith(':DVD') or \
+ (adi is not None and self.is_adi_project(project) != adi):
+ continue
+ projects.append(self.extract_staging_short(project))
+ return projects
+
+ def is_adi_project(self, p):
+ return ':adi:' in p
# this function will crash if given a non-adi project name
def extract_adi_number(self, p):
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment