From bd606390a95b6364b90dedcc4bfcbd6d3ad76dd6 Mon Sep 17 00:00:00 2001 From: Hans Ulrich Niedermann Date: Mon, 9 Aug 2010 15:14:08 +0200 Subject: Add "fedpkg initial-merge" command usage: fedpkg.py initial-merge [-h] [-n] [repo-path [repo-path ...]] Performs a 'git merge' of all git branches with the same content (i.e. with the same package spec files, patch files, etc.), regardless of their history. This is useful after Fedora's dist-cvs to dist-git migration, as often different branches have different histories but the same content on the filesystem. After these initial merges of identical trees, future merges between the branches will be a lot easier: Easier to follow in the dependency graph, and easier to perform without conflicts. positional arguments: repo-path Path to a repo to initial-merge optional arguments: -h, --help show this help message and exit -n, --dry-run Whether to run without actually merging --- src/fedpkg.bash | 5 +- src/fedpkg.py | 3 + src/pyfedpkg/__init__.py | 2 + src/pyfedpkg/initial_merge.py | 231 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100755 src/pyfedpkg/initial_merge.py diff --git a/src/fedpkg.bash b/src/fedpkg.bash index 117bd23..2331f98 100644 --- a/src/fedpkg.bash +++ b/src/fedpkg.bash @@ -36,7 +36,7 @@ _fedpkg() local options="--help -v -q" local options_value="--user --path" local commands="build chain-build ci clean clog clone co commit compile diff gimmespec giturl help \ - import install lint local mockbuild new new-sources patch prep pull push retire scratch-build sources \ + import initial-merge install lint local mockbuild new new-sources patch prep pull push retire scratch-build sources \ srpm switch-branch tag tag-request unused-patches update upload verrel" # parse main options and get command @@ -112,6 +112,9 @@ _fedpkg() options_branch="-b" after="package" ;; + initial-merge) + options="--dry-run" + ;; commit|ci) options="--push --clog --tag" options_string="--message" diff --git a/src/fedpkg.py b/src/fedpkg.py index 4bd565f..c897b4c 100755 --- a/src/fedpkg.py +++ b/src/fedpkg.py @@ -1039,6 +1039,9 @@ packages will be built sequentially. help = 'Source rpm to import') parser_import_srpm.set_defaults(command = import_srpm) + # Initial branch merges + pyfedpkg.initial_merge.add_parser_to(subparsers) + # install locally parser_install = subparsers.add_parser('install', help = 'Local test rpmbuild install') diff --git a/src/pyfedpkg/__init__.py b/src/pyfedpkg/__init__.py index a1edb35..51982ea 100644 --- a/src/pyfedpkg/__init__.py +++ b/src/pyfedpkg/__init__.py @@ -31,6 +31,8 @@ import OpenSSL import fnmatch import offtrac +from . import initial_merge + # Define some global variables, put them here to make it easy to change LOOKASIDE = 'http://pkgs.fedoraproject.org/repo/pkgs' diff --git a/src/pyfedpkg/initial_merge.py b/src/pyfedpkg/initial_merge.py new file mode 100755 index 0000000..4964745 --- /dev/null +++ b/src/pyfedpkg/initial_merge.py @@ -0,0 +1,231 @@ +# initial_merge.py - perform initial merge after dist-git migration +# +# Copyright (C) 2010 Hans Ulrich Niedermann +# +# 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/copyleft/gpl.html for +# the full text of the license. + + +# Note that this docstring is used as description for the command. +"""\ +Performs a 'git merge' of all git branches with the same content +(i.e. with the same package spec files, patch files, etc.), regardless +of their history. + +This is useful after Fedora's dist-cvs to dist-git migration, as often +different branches have different histories but the same content on the +filesystem. + +After these initial merges of identical trees, future merges between +the branches will be a lot easier: Easier to follow in the dependency +graph, and easier to perform without conflicts. +""" + +import argparse +import sys +import os +import subprocess +import logging + +import git +import pyfedpkg + + +__all__ = [ + 'add_parser_to', + 'handle_repo', + ] + + +merge_commit_msg = """\ +Initial pseudo merge for dist-git setup + +This git merge exists to make future git merges much easier. +""" + + +log = logging.getLogger("fedpkg") + + +def str_numsplit(s): + """Helper function for properly ordering branch names. + + Converts branch names like 'fc6', 'f8', 'f10', 'master' into + ('fc',6), ('f',8), ('f',10), 'master', respectively. + """ + assert(not s[0].isdigit()) + if not s[-1].isdigit(): + return s + i = len(s) + while i>0: + i = i - 1 + if not s[i].isdigit(): + break + prefix, numstr = s[0:i+1], s[i+1:] + return (prefix, int(numstr)) + + +def cmp_relbranch(a, b): + """Comparison function release branch names (for sort() calls) + + Sorts branch names like 'el4', 'fc6', 'f8', 'f10', 'master' in + that order. + """ + asplit = str_numsplit(a) + bsplit = str_numsplit(b) + if type(asplit) == str and type(bsplit) == str: + return cmp(asplit,bsplit) + elif type(asplit) == str and type(bsplit) == tuple: + return 1 + elif type(asplit) == tuple and type(bsplit) == str: + return -1 + elif type(asplit) == tuple and type(bsplit) == tuple: + if asplit[0] in ('f','fc') and bsplit[0] in ('f','fc'): + return cmp(asplit[1], bsplit[1]) + else: + return cmp(asplit,bsplit) + else: + return cmp(asplit,bsplit) + + +class Branch(object): + + """Convenience class for handling branches to initial-merge""" + + def __init__(self, sha, origbranch): + self.sha, self.origbranch = sha, origbranch + a = self.origbranch.split('/') + assert(a[0] == 'origin') + assert(a[-1] == 'master') + self.localbranch = a[1] + + def __repr__(self): + return "%(sha)s %(origbranch)s" % self.__dict__ + + def __cmp__(self, other): + if self.sha == other.sha: + return cmp_relbranch(self.localbranch, other.localbranch) + else: + return cmp(self.sha, other.sha) + + +class Filter(object): + + """Branch filter + + Feed branches to this filter via eat(), and flush() it after you + are done. The filter will detect branches with the same tree sha, + and call do_initial_merge() for them. + """ + + def __init__(self, repo, dry_run=False): + self.repo = repo + self.dry_run = dry_run + self.__reset() + + def __reset(self): + self.branch_list = [] + + def do_initial_merge(self, into, to_merge): + log.info("#### Merging %s into %s ####", + [ x.localbranch for x in to_merge ], into.localbranch) + pyfedpkg.switch_branch(into.localbranch, self.repo.working_tree_dir) + log.info("Merging %s into %s", + repr([ x.origbranch for x in to_merge]), repr(into.localbranch)) + if not self.dry_run: + self.repo.git.merge('-m', merge_commit_msg, + '-s', 'ours', + *[x.origbranch for x in to_merge]) + for t in to_merge: + pyfedpkg.switch_branch(t.localbranch, self.repo.working_tree_dir) + log.info("Merging %s into %s", repr(into.localbranch), repr(t.localbranch)) + if not self.dry_run: + self.repo.git.merge(into.localbranch) + pyfedpkg.switch_branch(into.localbranch, self.repo.working_tree_dir) + + def flush(self): + if len(self.branch_list) < 2: + return + + head = self.branch_list[-1] + others = self.branch_list[:-1] + self.do_initial_merge(head, others) + + self.__reset() + + def eat(self, item): + """Feed item to the filter. The filter will decide what to do with it.""" + if self.branch_list: + last = self.branch_list[-1] + if last.sha == item.sha: + self.branch_list.extend([item]) + else: + self.flush() + self.branch_list = [item] + else: + self.branch_list = [item] + + +def handle_repo(repo, dry_run=False): + log.info("######## initial-merge %s ########" % repo.working_tree_dir) + _locals, remotes = pyfedpkg._list_branches(repo=repo) + aa = [ Branch(repo.git.rev_parse('%s^{tree}' % b), b) for b in remotes ] + aa.sort() + log.info("Branches sorted by tree sha:") + for x in aa: + log.info(" %s" % x) + + n = 0 + f = Filter(repo, dry_run=dry_run) + while n < len(aa): + f.eat(aa[n]) + n = n + 1 + f.flush() + + log.info("######## /initial-merge %s ########" % repo.working_tree_dir) + + +class DirListAction(argparse.Action): + + def __init__(self, *args, **kwargs): + super(DirListAction, self).__init__(*args, **kwargs) + self.default_value = ['.'] + + def __call__(self, parser, namespace, values, option_string=None): + if values: + setattr(namespace, self.dest, values) + else: + setattr(namespace, self.dest, self.default) + + +def fedpkg_command(args): + is_first = True + for repo_path in args.repo_path: + if is_first: + is_first = False + else: + log.info("") + repo = git.Repo(repo_path) + handle_repo(repo, dry_run=args.dry_run) + + +_module_doc = __doc__ + + +def add_parser_to(subparsers): + sp = subparsers.add_parser('initial-merge', + help = 'git merge to join branches with identical trees', + formatter_class=argparse.RawDescriptionHelpFormatter, + description = _module_doc) + sp.add_argument('repo_path', metavar='repo-path', + nargs='*', default=['.'], + action=DirListAction, + help = 'Path to a repo to initial-merge') + sp.add_argument('-n', '--dry-run', + default=False, const=True, action='store_const', + help="Whether to run without actually merging") + sp.set_defaults(command = fedpkg_command) + return sp -- cgit