|
87 | 87 |
|
88 | 88 | # External dependencies.
|
89 | 89 | from executor import execute, quote
|
90 |
| -from humanfriendly import coerce_boolean, compact, concatenate, format, format_path, parse_path |
| 90 | +from humanfriendly import Timer, coerce_boolean, compact, concatenate, format, format_path, parse_path, pluralize |
91 | 91 | from natsort import natsort, natsort_key
|
92 | 92 | from property_manager import PropertyManager, lazy_property, required_property, writable_property
|
93 | 93 | from six import string_types
|
|
104 | 104 | )
|
105 | 105 |
|
106 | 106 | # Semi-standard module versioning.
|
107 |
| -__version__ = '0.28' |
| 107 | +__version__ = '0.29' |
108 | 108 |
|
109 | 109 | USER_CONFIG_FILE = os.path.expanduser('~/.vcs-repo-mgr.ini')
|
110 | 110 | """The absolute pathname of the user-specific configuration file (a string)."""
|
@@ -872,6 +872,100 @@ def merge(self, revision=None):
|
872 | 872 | **self.get_author()
|
873 | 873 | ))
|
874 | 874 |
|
| 875 | + def merge_up(self, target_branch=None, feature_branch=None, delete=True): |
| 876 | + """ |
| 877 | + Merge a change into one or more release branches and the default branch. |
| 878 | +
|
| 879 | + :param target_branch: The name of the release branch where merging of |
| 880 | + the feature branch starts (a string or |
| 881 | + :data:`None`, defaults to |
| 882 | + :attr:`current_branch`). |
| 883 | + :param feature_branch: The feature branch to merge in (a string or |
| 884 | + :data:`None`). Strings are parsed using |
| 885 | + :class:`FeatureBranchSpec`. |
| 886 | + :param delete: :data:`True` (the default) to delete or close the |
| 887 | + feature branch after it is merged, :data:`False` |
| 888 | + otherwise. |
| 889 | + :returns: If `feature_branch` is given the global revision id of the |
| 890 | + feature branch is returned, otherwise the global revision id |
| 891 | + of the target branch (before any merges performed by |
| 892 | + :func:`merge_up()`) is returned. |
| 893 | + :raises: The following exceptions can be raised: |
| 894 | +
|
| 895 | + - :exc:`~exceptions.TypeError` when `target_branch` and |
| 896 | + :attr:`current_branch` are both :data:`None`. |
| 897 | + - :exc:`~exceptions.ValueError` when the given target branch |
| 898 | + doesn't exist (based on :attr:`branches`). |
| 899 | + """ |
| 900 | + timer = Timer() |
| 901 | + # Validate the target branch or select the default target branch. |
| 902 | + if target_branch: |
| 903 | + if target_branch not in self.branches: |
| 904 | + raise ValueError("The target branch %r doesn't exist!" % target_branch) |
| 905 | + else: |
| 906 | + target_branch = self.current_branch |
| 907 | + if not target_branch: |
| 908 | + raise TypeError("You need to specify the target branch! (where merging starts)") |
| 909 | + # Parse the feature branch specification. |
| 910 | + feature_branch = FeatureBranchSpec(feature_branch) if feature_branch else None |
| 911 | + # Make sure we start with a clean working tree. |
| 912 | + self.ensure_clean() |
| 913 | + # Make sure we're up to date with our upstream repository (if any). |
| 914 | + self.update() |
| 915 | + # Check out the target branch. |
| 916 | + self.checkout(revision=target_branch) |
| 917 | + # Get the global revision id of the release branch we're about to merge. |
| 918 | + revision_to_merge = self.find_revision_id(target_branch) |
| 919 | + # Check if we need to merge in a feature branch. |
| 920 | + if feature_branch: |
| 921 | + if feature_branch.location: |
| 922 | + # Pull in the feature branch. |
| 923 | + self.update(remote=feature_branch.location) |
| 924 | + # Get the global revision id of the feature branch we're about to merge. |
| 925 | + revision_to_merge = self.find_revision_id(feature_branch.revision) |
| 926 | + # Merge in the feature branch. |
| 927 | + self.merge(revision=feature_branch.revision) |
| 928 | + # Commit the merge. |
| 929 | + self.commit(message="Merged %s" % feature_branch.expression) |
| 930 | + # Find the release branches in the repository. |
| 931 | + release_branches = [release.revision.branch for release in self.ordered_releases] |
| 932 | + logger.debug("Found %s: %s", |
| 933 | + pluralize(len(release_branches), "release branch", "release branches"), |
| 934 | + concatenate(release_branches)) |
| 935 | + # Find the release branches after the target branch. |
| 936 | + later_branches = release_branches[release_branches.index(target_branch) + 1:] |
| 937 | + logger.info("Found %s after target branch (%s): %s", |
| 938 | + pluralize(len(later_branches), "release branch", "release branches"), |
| 939 | + target_branch, |
| 940 | + concatenate(later_branches)) |
| 941 | + # Determine the branches that need to be merged. |
| 942 | + branches_to_upmerge = later_branches + [self.default_revision] |
| 943 | + logger.info("Merging up from %s to %s: %s", |
| 944 | + target_branch, |
| 945 | + pluralize(len(branches_to_upmerge), "branch", "branches"), |
| 946 | + concatenate(branches_to_upmerge)) |
| 947 | + # Merge the feature branch up through the selected branches. |
| 948 | + merge_queue = [target_branch] + branches_to_upmerge |
| 949 | + while len(merge_queue) >= 2: |
| 950 | + from_branch = merge_queue[0] |
| 951 | + to_branch = merge_queue[1] |
| 952 | + logger.info("Merging %s into %s ..", from_branch, to_branch) |
| 953 | + self.checkout(revision=to_branch) |
| 954 | + self.merge(revision=from_branch) |
| 955 | + self.commit(message="Merged %s" % from_branch) |
| 956 | + merge_queue.pop(0) |
| 957 | + # Check if we need to delete or close the feature branch. |
| 958 | + if delete and feature_branch and feature_branch.revision in self.branches: |
| 959 | + # Delete or close the feature branch. |
| 960 | + self.delete_branch( |
| 961 | + branch_name=feature_branch.revision, |
| 962 | + message="Closing feature branch %s" % feature_branch.revision, |
| 963 | + ) |
| 964 | + # Update the working tree to the default branch. |
| 965 | + self.checkout() |
| 966 | + logger.info("Done! Finished merging up in %s.", timer) |
| 967 | + return revision_to_merge |
| 968 | + |
875 | 969 | def add_files(self, *pathnames, **kw):
|
876 | 970 | """
|
877 | 971 | Stage new files in the working tree to be included in the next commit.
|
@@ -1412,6 +1506,68 @@ def __repr__(self):
|
1412 | 1506 | ]))
|
1413 | 1507 |
|
1414 | 1508 |
|
| 1509 | +class FeatureBranchSpec(PropertyManager): |
| 1510 | + |
| 1511 | + """Simple and human friendly feature branch specifications.""" |
| 1512 | + |
| 1513 | + def __init__(self, expression): |
| 1514 | + """ |
| 1515 | + Initialize a :class:`FeatureBranchSpec` object. |
| 1516 | +
|
| 1517 | + :param expression: A feature branch specification (a string). |
| 1518 | +
|
| 1519 | + The `expression` string is parsed as follows: |
| 1520 | +
|
| 1521 | + - If `expression` contains two nonempty substrings separated by the |
| 1522 | + character ``#`` it is split into two parts where the first part is |
| 1523 | + used to set :attr:`location` and the second part is used to set |
| 1524 | + :attr:`revision`. |
| 1525 | + - Otherwise `expression` is interpreted as a revision without a |
| 1526 | + location (in this case :attr:`location` will be :data:`None`). |
| 1527 | +
|
| 1528 | + Some examples to make things more concrete: |
| 1529 | +
|
| 1530 | + >>> from vcs_repo_mgr import FeatureBranchSpec |
| 1531 | + >>> FeatureBranchSpec('https://github.com/xolox/python-vcs-repo-mgr.git#remote-feature-branch') |
| 1532 | + FeatureBranchSpec(expression='https://github.com/xolox/python-vcs-repo-mgr.git#remote-feature-branch', |
| 1533 | + location='https://github.com/xolox/python-vcs-repo-mgr.git', |
| 1534 | + revision='remote-feature-branch') |
| 1535 | + >>> FeatureBranchSpec('local-feature-branch') |
| 1536 | + FeatureBranchSpec(expression='local-feature-branch', |
| 1537 | + location=None, |
| 1538 | + revision='local-feature-branch') |
| 1539 | + """ |
| 1540 | + super(FeatureBranchSpec, self).__init__(expression=expression) |
| 1541 | + |
| 1542 | + @required_property |
| 1543 | + def expression(self): |
| 1544 | + """The feature branch specification provided by the user (a string).""" |
| 1545 | + |
| 1546 | + @writable_property |
| 1547 | + def location(self): |
| 1548 | + """ |
| 1549 | + The location of the repository that contains :attr:`revision` (a string or :data:`None`). |
| 1550 | +
|
| 1551 | + The computed default value of :attr:`location` is based on the value of |
| 1552 | + :attr:`expression` as described in the documentation of |
| 1553 | + :func:`__init__()`. |
| 1554 | + """ |
| 1555 | + location, _, revision = self.expression.partition('#') |
| 1556 | + return location if location and revision else None |
| 1557 | + |
| 1558 | + @required_property |
| 1559 | + def revision(self): |
| 1560 | + """ |
| 1561 | + The name of the feature branch (a string). |
| 1562 | +
|
| 1563 | + The computed default value of :attr:`revision` is based on the value of |
| 1564 | + :attr:`expression` as described in the documentation of |
| 1565 | + :func:`__init__()`. |
| 1566 | + """ |
| 1567 | + location, _, revision = self.expression.partition('#') |
| 1568 | + return revision if location and revision else self.expression |
| 1569 | + |
| 1570 | + |
1415 | 1571 | class HgRepo(Repository):
|
1416 | 1572 |
|
1417 | 1573 | """
|
|
0 commit comments