
When auto-detecting the most recent point in the history of the current branch to be updated that came from upstream, need to support searching more than the given upstream reference where the user is switching the upstream branch being tracked. Use a default of 'upstream/*' and add a CLI option to set a different pattern to be searched in cases where the repository is not using the same naming. The CLI option '--search-refs' may be specified multiple times to provide multiple ref patterns to be searched using simple globbing. The default if none provided by the user is 'upstream/*'. Change-Id: If890d1bee015d6b495a5caa60dd8f7783cf7c4e0 Closes-Bug: #1380652
658 lines
25 KiB
Python
658 lines
25 KiB
Python
#
|
|
# Copyright (c) 2012, 2013, 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
from git_upstream.errors import GitUpstreamError
|
|
from git_upstream.log import LogDedentMixin
|
|
from git_upstream.lib.utils import GitMixin
|
|
from git_upstream.lib.rebaseeditor import RebaseEditor
|
|
from git_upstream import subcommand, log
|
|
from git_upstream.lib.searchers import UpstreamMergeBaseSearcher
|
|
|
|
from abc import ABCMeta, abstractmethod
|
|
from collections import Sequence
|
|
from git import GitCommandError
|
|
|
|
import inspect
|
|
|
|
|
|
class ImportUpstreamError(GitUpstreamError):
|
|
"""Exception thrown by L{ImportUpstream}"""
|
|
pass
|
|
|
|
|
|
class ImportUpstream(LogDedentMixin, GitMixin):
|
|
"""
|
|
Import code from an upstream project and merge in additional branches
|
|
to create a new branch unto which changes that are not upstream but are
|
|
on the local branch are applied.
|
|
"""
|
|
|
|
def __init__(self, branch=None, upstream=None, import_branch=None,
|
|
extra_branches=None, *args, **kwargs):
|
|
if not extra_branches:
|
|
extra_branches = []
|
|
self._branch = branch
|
|
self._upstream = upstream
|
|
self._import_branch = import_branch
|
|
self._extra_branches = extra_branches
|
|
|
|
# make sure to correctly initialise inherited objects before performing
|
|
# any computation
|
|
super(ImportUpstream, self).__init__(*args, **kwargs)
|
|
|
|
# test that we can use this git repo
|
|
if not self.is_detached():
|
|
raise ImportUpstreamError("In 'detached HEAD' state")
|
|
|
|
if self.repo.bare:
|
|
raise ImportUpstreamError("Cannot perform imports in bare repos")
|
|
|
|
if self.branch == 'HEAD':
|
|
self._branch = str(self.repo.active_branch)
|
|
|
|
# validate branches exist and log all failures
|
|
branches = [
|
|
self.branch,
|
|
self.upstream
|
|
]
|
|
branches.extend(self.extra_branches)
|
|
|
|
invalid_ref = False
|
|
for branch in branches:
|
|
if not any(head for head in self.repo.heads
|
|
if head.name == branch):
|
|
msg = "Specified ref does not exist: '%s'"
|
|
self.log.error(msg, branch)
|
|
invalid_ref = True
|
|
|
|
if invalid_ref:
|
|
raise ImportUpstreamError("Invalid ref")
|
|
|
|
@property
|
|
def branch(self):
|
|
"""Branch to search for branch changes to apply when importing."""
|
|
return self._branch
|
|
|
|
@property
|
|
def upstream(self):
|
|
"""Branch containing the upstream project code base to track."""
|
|
return self._upstream
|
|
|
|
@property
|
|
def import_branch(self):
|
|
"""
|
|
Pattern to use to generate the name, or user specified branch name
|
|
to use for import.
|
|
"""
|
|
return self._import_branch
|
|
|
|
@property
|
|
def extra_branches(self):
|
|
"""
|
|
Branch containing the additional branches to be merged with the
|
|
upstream when importing.
|
|
"""
|
|
return self._extra_branches
|
|
|
|
def _set_branch(self, branch, commit, checkout=False, force=False):
|
|
|
|
if str(self.repo.active_branch) == branch:
|
|
self.log.info(
|
|
"""\
|
|
Resetting branch '%s' to specified commit '%s'
|
|
git reset --hard %s
|
|
""", branch, commit, commit)
|
|
self.git.reset(commit, hard=True)
|
|
elif checkout:
|
|
if force:
|
|
checkout_opt = '-B'
|
|
else:
|
|
checkout_opt = '-b'
|
|
|
|
self.log.info(
|
|
"""\
|
|
Checking out branch '%s' using specified commit '%s'
|
|
git checkout %s %s %s
|
|
""", branch, commit, checkout_opt, branch, commit)
|
|
self.git.checkout(checkout_opt, branch, commit)
|
|
else:
|
|
self.log.info(
|
|
"""\
|
|
Creating branch '%s' from specified commit '%s'
|
|
git branch --force %s %s
|
|
""", branch, commit, branch, commit)
|
|
self.git.branch(branch, commit, force=force)
|
|
|
|
def create_import(self, commit=None, import_branch=None, checkout=False,
|
|
force=False):
|
|
"""
|
|
Create the import branch from the specified commit.
|
|
|
|
If the branch already exists abort if force is false
|
|
If current branch, reset the head to the specified commit
|
|
If checkout is true, switch and reset the branch to the commit
|
|
Otherwise just reset the branch to the specified commit
|
|
If the branch doesn't exist, create it and switch to it
|
|
automatically if checkout is true.
|
|
"""
|
|
|
|
if not commit:
|
|
commit = self.upstream
|
|
|
|
try:
|
|
self.git.show_ref(commit, quiet=True, heads=True)
|
|
|
|
except GitCommandError as e:
|
|
msg = "Invalid commit '%s' specified to import from"
|
|
self.log.error(msg, commit)
|
|
raise ImportUpstreamError((msg + ": %s"), commit, e)
|
|
|
|
if not import_branch:
|
|
import_branch = self.import_branch
|
|
|
|
# use describe in order to be certain about unique identifying 'commit'
|
|
# Create a describe string with the following format:
|
|
# <describe upstream>[-<extra branch abbref hash>]*
|
|
#
|
|
# Simply appends the 7 character ref abbreviation for each extra branch
|
|
# prefixed with '-', for each extra branch in the order they are given.
|
|
describe_commit = self.git.describe(commit, tags=True,
|
|
with_exceptions=False)
|
|
if not describe_commit:
|
|
self.log.warning("No tag describes the upstream branch")
|
|
describe_commit = self.git.describe(commit, always=True, tags=True)
|
|
|
|
self.log.info("""\
|
|
Using '%s' to describe:
|
|
%s
|
|
""", describe_commit, commit)
|
|
describe_branches = [describe_commit]
|
|
|
|
describe_branches.extend([self.git.rev_parse(b, short=True)
|
|
for b in self.extra_branches])
|
|
import_describe = "-".join(describe_branches)
|
|
self._import_branch = self.import_branch.format(
|
|
describe=import_describe)
|
|
|
|
self._import_branch = import_branch.format(describe=import_describe)
|
|
base = self._import_branch + "-base"
|
|
self.log.debug("Creating and switching to import branch base '%s' "
|
|
"created from '%s' (%s)", base, self.upstream, commit)
|
|
|
|
self.log.info(
|
|
"""\
|
|
Checking if import branch '%s' already exists:
|
|
git branch --list %s
|
|
""", base, base)
|
|
if self.git.show_ref("refs/heads/" + base, verify=True,
|
|
with_exceptions=False) and not force:
|
|
msg = "Import branch '%s' already exists, set 'force' to replace"
|
|
self.log.error(msg, self.import_branch)
|
|
raise ImportUpstreamError(msg % self.import_branch)
|
|
|
|
self._set_branch(base, commit, checkout, force)
|
|
|
|
if self.extra_branches:
|
|
self.log.info(
|
|
"""\
|
|
Merging additional branch(es) '%s' into import branch '%s'
|
|
git checkout %s
|
|
git merge %s
|
|
""", ", ".join(self.extra_branches), base, base,
|
|
" ".join(self.extra_branches))
|
|
self.git.checkout(base)
|
|
self.git.merge(*self.extra_branches)
|
|
|
|
def _linearise(self, branch, sequence, previous_import):
|
|
|
|
counter = len(sequence) - 1
|
|
ancestors = set()
|
|
|
|
self._set_branch(branch, previous_import, checkout=True, force=True)
|
|
root = previous_import.hexsha
|
|
while counter > 0:
|
|
# add commit to list of ancestors to check
|
|
ancestors.add(root)
|
|
|
|
# look for merge commits that are not part of ancestry path
|
|
for idx in xrange(counter - 1, -1, -1):
|
|
commit = sequence[idx]
|
|
# if there is only one parent, no need to check the others
|
|
if len(commit.parents) < 2:
|
|
ancestors.add(commit.hexsha)
|
|
elif any(p.hexsha not in ancestors for p in commit.parents):
|
|
self.log.debug("Rebase upto commit SHA1: %s",
|
|
commit.hexsha)
|
|
idx = idx + 1
|
|
break
|
|
else:
|
|
ancestors.add(commit.hexsha)
|
|
tip = sequence[idx].hexsha
|
|
|
|
self.log.info("Rebasing from %s to %s", root, tip)
|
|
previous = self.git.rev_parse(branch)
|
|
self.log.info("Rebasing onto '%s'", previous)
|
|
if root == previous and idx == 0:
|
|
# special case, we are already linear
|
|
self.log.info("Already in a linear layout")
|
|
return
|
|
self._set_branch(branch, tip, force=True)
|
|
try:
|
|
self.log.debug(
|
|
"""\
|
|
git rebase -p --onto=%s \\
|
|
%s %s
|
|
""", previous, root, branch)
|
|
self.git.rebase(root, branch, onto=previous, p=True)
|
|
except:
|
|
self.git.rebase(abort=True, with_exceptions=False)
|
|
raise
|
|
counter = idx - 1
|
|
# set root commit for next loop
|
|
root = sequence[counter].hexsha
|
|
|
|
def apply(self, strategy, interactive=False):
|
|
"""Apply list of commits given onto latest import of upstream"""
|
|
|
|
commit_list = list(strategy.filtered_iter())
|
|
if len(commit_list) == 0:
|
|
self.log.notice("There are no local changes to be applied!")
|
|
return False
|
|
|
|
self.log.debug(
|
|
"""\
|
|
Should apply the following list of commits
|
|
%s
|
|
""", "\n ".join([c.hexsha for c in commit_list]))
|
|
|
|
base = self.import_branch + "-base"
|
|
|
|
self._set_branch(self.import_branch, self.branch, force=True)
|
|
self.log.info(
|
|
"""\
|
|
Creating import branch '%s' from specified commit '%s' in prep to
|
|
linearize the local changes before transposing to the new upstream:
|
|
git branch --force %s %s
|
|
""", self.import_branch, self.branch, self.import_branch,
|
|
self.branch)
|
|
|
|
self.log.notice("Attempting to linearise previous changes")
|
|
# attempt to silently linearize the current carried changes as a branch
|
|
# based on the previous located import commit. This provides a sane
|
|
# abort result for if the user needs to abort the rebase of this branch
|
|
# onto the new point upstream that was requested to import from.
|
|
try:
|
|
self._linearise(self.import_branch, strategy,
|
|
strategy.searcher.commit)
|
|
except:
|
|
# Could ask user if they want to try and use the non clean route
|
|
# provided they don't mind that 'git rebase --abort' will result
|
|
# in a virtually useless local import branch
|
|
self.log.warning(
|
|
"""\
|
|
|
|
Exception occurred during linearisation of local changes on to
|
|
previous import to simplify behaviour should user need to abort
|
|
the rebase that applies these changes to the latest import
|
|
point. Attempting to tidy up state.
|
|
|
|
Do not Ctrl+C unless you wish to need to clean up your git
|
|
repository by hand.
|
|
|
|
""")
|
|
# reset head back to the tip of the changes to be rebased
|
|
self._set_branch(self.import_branch, self.branch, force=True)
|
|
|
|
rebase = RebaseEditor(interactive, repo=self.repo)
|
|
if len(commit_list):
|
|
first = commit_list[0]
|
|
|
|
self.log.info(
|
|
"""\
|
|
Rebase changes, dropping merges through editor:
|
|
git rebase --onto %s \\
|
|
%s %s
|
|
""", base, first.parents[0].hexsha, self.import_branch)
|
|
status, out, err = rebase.run(commit_list,
|
|
first.parents[0].hexsha,
|
|
self.import_branch,
|
|
onto=base)
|
|
if status:
|
|
if err and err.startswith("Nothing to do"):
|
|
# cancelled by user
|
|
self.log.notice("Cancelled by user")
|
|
return False
|
|
|
|
self.log.error("Rebase failed, will need user intervention to "
|
|
"resolve.")
|
|
if out:
|
|
self.log.notice(out)
|
|
if err:
|
|
self.log.notice(err)
|
|
|
|
# once we support resuming/finishing add a message here to tell
|
|
# the user to rerun this tool with the appropriate options to
|
|
# complete
|
|
return False
|
|
|
|
self.log.notice("Successfully applied all locally carried changes")
|
|
else:
|
|
self.log.warning("Warning, nothing to do: locally carried " +
|
|
"changes already rebased onto " + self.upstream)
|
|
return True
|
|
|
|
def resume(self, args):
|
|
"""Resume previous partial import"""
|
|
raise NotImplementedError
|
|
|
|
def finish(self):
|
|
"""
|
|
Finish merge according to the selected strategy while performing
|
|
suitable verification checks.
|
|
"""
|
|
self.log.info("No verification checks enabled")
|
|
self.git.checkout(self.branch)
|
|
current_sha = self.git.rev_parse("HEAD")
|
|
|
|
try:
|
|
self.log.info(
|
|
"""\
|
|
Merging by inverting the 'ours' strategy discard all changes
|
|
and replace existing branch contents with the new import.
|
|
""")
|
|
self.log.info(
|
|
"""\
|
|
Merging import branch to HEAD and ignoring changes:
|
|
git merge -s ours --no-commit %s
|
|
""", self.import_branch)
|
|
self.git.merge('-s', 'ours', self.import_branch, no_commit=True)
|
|
self.log.info(
|
|
"""\
|
|
Replacing tree contents with those from the import branch:
|
|
git read-tree -u --reset %s
|
|
""", self.import_branch)
|
|
self.git.read_tree(self.import_branch, u=True, reset=True)
|
|
self.log.info(
|
|
"""\
|
|
Committing merge commit:
|
|
git commit --no-edit
|
|
""")
|
|
self.git.commit(no_edit=True)
|
|
# finally test that everything worked correctly by comparing if
|
|
# the tree object id's match
|
|
if self.git.rev_parse("HEAD^{tree}") != \
|
|
self.git.rev_parse("%s^{tree}" % self.import_branch):
|
|
raise ImportUpstreamError(
|
|
"Resulting tree does not match import")
|
|
except (GitCommandError, ImportUpstreamError):
|
|
self.log.error(
|
|
"""\
|
|
Failed to finish import by merging branch:
|
|
'%s'
|
|
into and replacing the contents of:
|
|
'%s'
|
|
""", self.import_branch, self.branch)
|
|
self._set_branch(self.branch, current_sha, force=True)
|
|
return False
|
|
except:
|
|
self.log.exception("Unknown exception during finish")
|
|
self._set_branch(self.branch, current_sha, force=True)
|
|
raise
|
|
return True
|
|
|
|
|
|
class ImportStrategiesFactory(object):
|
|
__strategies = None
|
|
|
|
@classmethod
|
|
def create_strategy(cls, type, *args, **kwargs):
|
|
if type in cls.list_strategies():
|
|
return cls.__strategies[type](*args, **kwargs)
|
|
else:
|
|
raise RuntimeError("No class implements the requested strategy: "
|
|
"{0}".format(type))
|
|
|
|
@classmethod
|
|
def list_strategies(cls):
|
|
cls.__strategies = {
|
|
subclass._strategy: subclass
|
|
for subclass in LocateChangesStrategy.__subclasses__()
|
|
if subclass._strategy}
|
|
return cls.__strategies.keys()
|
|
|
|
|
|
from git_upstream.lib.searchers import (NoMergeCommitFilter,
|
|
ReverseCommitFilter,
|
|
DiscardDuplicateGerritChangeId,
|
|
SupersededCommitFilter,
|
|
DroppedCommitFilter)
|
|
|
|
|
|
class LocateChangesStrategy(GitMixin, Sequence):
|
|
"""
|
|
Base class that needs to be extended with the specific strategy on how to
|
|
handle changes locally that are not yet upstream.
|
|
"""
|
|
__metaclass__ = ABCMeta
|
|
|
|
@abstractmethod
|
|
def __init__(self, git=None, *args, **kwargs):
|
|
"""
|
|
Initialize an empty filters list
|
|
"""
|
|
self.data = None
|
|
self.filters = []
|
|
super(LocateChangesStrategy, self).__init__(*args, **kwargs)
|
|
|
|
def __getitem__(self, key):
|
|
if not self.data:
|
|
self.data = self._popdata()
|
|
return self.data[key]
|
|
|
|
def __len__(self):
|
|
if not self.data:
|
|
self.data = self._popdata()
|
|
return len(self.data)
|
|
|
|
@classmethod
|
|
def get_strategy_name(cls):
|
|
return cls._strategy
|
|
|
|
def filtered_iter(self):
|
|
# chain the filters as generators so that we don't need to allocate new
|
|
# lists for each step in the filter chain.
|
|
commit_list = self
|
|
for f in self.filters:
|
|
commit_list = f.filter(commit_list)
|
|
|
|
return commit_list
|
|
|
|
def filtered_list(self):
|
|
|
|
return list(self.filtered_iter())
|
|
|
|
def _popdata(self):
|
|
"""
|
|
Should return the list of commits from the searcher object
|
|
"""
|
|
return self.searcher.list()
|
|
|
|
|
|
class LocateChangesWalk(LocateChangesStrategy):
|
|
"""
|
|
"""
|
|
|
|
_strategy = "drop"
|
|
|
|
def __init__(self, branch="HEAD", upstream="upstream/master",
|
|
search_refs=None, *args, **kwargs):
|
|
|
|
if not search_refs:
|
|
search_refs = []
|
|
search_refs.insert(0, upstream)
|
|
|
|
self.searcher = UpstreamMergeBaseSearcher(branch=branch,
|
|
patterns=search_refs)
|
|
self.upstream = upstream
|
|
super(LocateChangesWalk, self).__init__(*args, **kwargs)
|
|
|
|
def filtered_iter(self):
|
|
# may wish to make class used to remove duplicate objects configurable
|
|
# through git-upstream specific 'git config' settings
|
|
self.filters.append(
|
|
DiscardDuplicateGerritChangeId(self.upstream,
|
|
limit=self.searcher.commit))
|
|
self.filters.append(NoMergeCommitFilter())
|
|
self.filters.append(ReverseCommitFilter())
|
|
self.filters.append(DroppedCommitFilter())
|
|
self.filters.append(
|
|
SupersededCommitFilter(self.upstream,
|
|
limit=self.searcher.commit))
|
|
|
|
return super(LocateChangesWalk, self).filtered_iter()
|
|
|
|
|
|
@subcommand.arg('-d', '--dry-run', dest='dry_run', action='store_true',
|
|
default=False,
|
|
help='Only print out the list of commits that would be '
|
|
'applied.')
|
|
@subcommand.arg('-i', '--interactive', action='store_true', default=False,
|
|
help='Let the user edit the list of commits before applying.')
|
|
@subcommand.arg('-f', '--force', dest='force', required=False,
|
|
action='store_true', default=False,
|
|
help='Force overwrite of existing import branch if it exists.')
|
|
@subcommand.arg('--merge', dest='merge', required=False, action='store_true',
|
|
default=True,
|
|
help='Merge the resulting import branch into the target branch'
|
|
' once complete')
|
|
@subcommand.arg('--no-merge', dest='merge', required=False,
|
|
action='store_false',
|
|
help="Disable merge of the resulting import branch")
|
|
@subcommand.arg('-s', '--strategy', metavar='<strategy>',
|
|
choices=ImportStrategiesFactory.list_strategies(),
|
|
default=LocateChangesWalk.get_strategy_name(),
|
|
help='Use the given strategy to re-apply locally carried '
|
|
'changes to the import branch. (default: %(default)s)')
|
|
@subcommand.arg('--search-refs', action='append_replace', metavar='<pattern>',
|
|
default=['upstream/*'], dest='search_refs',
|
|
help='Refs to search for previous import commit. May be '
|
|
'specified multiple times.')
|
|
@subcommand.arg('--into', dest='branch', metavar='<branch>', default='HEAD',
|
|
help='Branch to take changes from, and replace with imported '
|
|
'branch.')
|
|
@subcommand.arg('--import-branch', metavar='<import-branch>',
|
|
help='Name of import branch to use',
|
|
default='import/{describe}')
|
|
@subcommand.arg('upstream_branch', metavar='<upstream-branch>', nargs='?',
|
|
default='upstream/master',
|
|
help='Upstream branch to import. Must be specified if '
|
|
'you wish to provide additional branches.')
|
|
@subcommand.arg('branches', metavar='<branches>', nargs='*',
|
|
help='Branches to additionally merge into the import branch '
|
|
'using default git merging behaviour')
|
|
def do_import(args):
|
|
"""
|
|
Import code from specified upstream branch.
|
|
|
|
Creates an import branch from the specified upstream branch, and optionally
|
|
merges additional branches given as arguments. Current branch, unless
|
|
overridden by the --into option, is used as the target branch from which a
|
|
list of changes to apply onto the new import is constructed based on the
|
|
the specified strategy.
|
|
|
|
Once complete it will merge and replace the contents of the target branch
|
|
with those from the import branch, unless --no-merge is specified.
|
|
"""
|
|
|
|
logger = log.get_logger('%s.%s' % (__name__,
|
|
inspect.stack()[0][0].f_code.co_name))
|
|
|
|
import_upstream = ImportUpstream(branch=args.branch,
|
|
upstream=args.upstream_branch,
|
|
import_branch=args.import_branch,
|
|
extra_branches=args.branches)
|
|
|
|
logger.notice("Searching for previous import")
|
|
strategy = ImportStrategiesFactory.create_strategy(
|
|
args.strategy, branch=args.branch, upstream=args.upstream_branch,
|
|
search_refs=args.search_refs)
|
|
|
|
if len(strategy) == 0:
|
|
raise ImportUpstreamError("Cannot find previous import")
|
|
|
|
# if last commit in the strategy was a merge, then the additional branches
|
|
# that were merged in previously can be extracted based on the commits
|
|
# merged.
|
|
prev_import_merge = strategy[-1]
|
|
if len(prev_import_merge.parents) > 1:
|
|
idxs = [idx for idx, commit in enumerate(prev_import_merge.parents)
|
|
if commit.hexsha != strategy.searcher.commit.hexsha]
|
|
|
|
if idxs:
|
|
additional_commits = [prev_import_merge.parents[i] for i in idxs]
|
|
if additional_commits and len(args.branches) == 0:
|
|
logger.warning("""\
|
|
**************** WARNING ****************
|
|
Previous import merged additional branches but non have
|
|
been specified on the command line for this import.\n""")
|
|
|
|
if args.dry_run:
|
|
commit_list = [c.hexsha[:6] + " - " + c.summary[:60] +
|
|
(c.summary[60:] and "...")
|
|
for c in list(strategy.filtered_iter())]
|
|
logger.notice("""\
|
|
Requested a dry-run: printing the list of commit that should be
|
|
rebased
|
|
|
|
%s
|
|
""", "\n ".join(commit_list))
|
|
return True
|
|
|
|
logger.notice("Starting import of upstream")
|
|
import_upstream.create_import(force=args.force)
|
|
logger.notice("Successfully created import branch")
|
|
|
|
if not import_upstream.apply(strategy, args.interactive):
|
|
logger.notice("Import cancelled")
|
|
return False
|
|
|
|
if not args.merge:
|
|
logger.notice(
|
|
"""\
|
|
Import complete, not merging to target branch '%s' as requested.
|
|
""", args.branch)
|
|
return True
|
|
|
|
logger.notice("Merging import to requested branch '%s'", args.branch)
|
|
if import_upstream.finish():
|
|
logger.notice(
|
|
"""\
|
|
Successfully finished import:
|
|
target branch: '%s'
|
|
upstream branch: '%s'
|
|
import branch: '%s'""", args.branch, args.upstream_branch,
|
|
import_upstream.import_branch)
|
|
if args.branches:
|
|
for branch in args.branches:
|
|
logger.notice(" extra branch: '%s'", branch, dedent=False)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
# vim:sw=4:sts=4:ts=4:et:
|