# 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', ] 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', 'Initial peudo merge for dist-git setup', '-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] class UnknownRepoTypeError(pyfedpkg.FedpkgError): pass def handle_repo(repo, dry_run=False): if type(repo) == str: repo = git.Repo(repo) elif isinstance(repo, git.Repo): pass else: raise UnknownRepoTypeError("%s" % repo) 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