def rename_current_branch(new_branch_name): # type: (str) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: git("checkout -b %s" % new_branch_name) git("branch -d %s" % current_branch) tracker.rename_branch(current_branch, new_branch_name)
def rename_current_branch(new_branch_name): # type: (Text) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: git("checkout -b {}".format(new_branch_name)) git("branch -d {}".format(current_branch)) tracker.rename_branch(current_branch, new_branch_name)
def _test_delete_archived_branches(target_directory): # type: (Text) -> None with _run_test(target_directory): _initialize_repo() # Create all the branches make_child_branch("first_branch") assert get_current_branch() == "first_branch" git("checkout master") make_child_branch("second_branch") assert get_current_branch() == "second_branch" make_child_branch("second_branch_child_one") assert get_current_branch() == "second_branch_child_one" make_child_branch("second_branch_child_two") assert get_current_branch() == "second_branch_child_two" git("checkout master") with get_branch_tracker() as tracker: tracker.set_is_archived("first_branch", True) tracker.set_is_archived("second_branch_child_two", True) # Test delete_archived_branches() with get_branch_tracker() as tracker: assert not tracker.is_branch_tracked("first_branch") assert tracker.is_branch_tracked("second_branch") assert tracker.is_branch_tracked("second_branch_child_one") assert not tracker.is_branch_tracked("second_branch_child_two")
def rebase_children(is_recursive, extra_git_rebase_args=()): # type: (bool, Sequence[Text]) -> None current_branch = get_current_branch() if extra_git_rebase_args: # If the first extra arg starts with "-", "--" must also have been passed, and # argparse doesn't remove it for us if extra_git_rebase_args[0] == "--": extra_git_rebase_args = extra_git_rebase_args[1:] extra_args = " " + " ".join(extra_git_rebase_args) else: extra_args = "" with get_branch_tracker() as tracker: do_rebase(tracker, tracker.parent_for_child(current_branch), current_branch, extra_args) if is_recursive: to_rebase_onto = [current_branch] while to_rebase_onto: parent = to_rebase_onto.pop() children = tracker.children_for_parent(parent) for child in children: do_rebase(tracker, parent, child, extra_args) to_rebase_onto.append(child) # Go back to where we started. git("checkout {}".format(current_branch))
def do_rebase(tracker, parent, child): # type: (BranchTracker, str, str) -> None bases = tracker.bases_for_branch(child) if len(bases) == 2: first_base, second_base = bases has_first_base = does_branch_contain_commit(child, first_base) has_second_base = does_branch_contain_commit(child, second_base) # Should have at least one of the two bases assert has_first_base or has_second_base if has_first_base and has_second_base: # Choose the newer one. The older one will be the merge base of the two older_base = git("merge-base %s %s" % (first_base, second_base)) first_is_newer = older_base == second_base base = first_base if first_is_newer else second_base else: # Only has one, choose the one that it does have base = bases[0] if has_first_base else bases[1] tracker.finish_rebase(child, base) else: base = bases[0] parent_rev = hash_for(parent) tracker.start_rebase(child, parent_rev) git("rebase --onto %s %s %s" % (parent, base, child)) tracker.finish_rebase(child, parent_rev)
def do_rebase(tracker, parent, child): # type: (BranchTracker, str, str) -> None bases = tracker.bases_for_branch(child) if len(bases) == 2: first_base, second_base = bases has_first_base = does_branch_contain_commit(child, first_base) has_second_base = does_branch_contain_commit(child, second_base) # Should have at least one of the two bases assert has_first_base or has_second_base if has_first_base and has_second_base: # Choose the newer one. The older one will be the merge base of the two older_base = git("merge-base {} {}".format(first_base, second_base)) first_is_newer = older_base == second_base base = first_base if first_is_newer else second_base else: # Only has one, choose the one that it does have base = bases[0] if has_first_base else bases[1] tracker.finish_rebase(child, base) else: base = bases[0] parent_rev = hash_for(parent) tracker.start_rebase(child, parent_rev) git("rebase --onto {} {} {}".format(parent, base, child)) tracker.finish_rebase(child, parent_rev)
def rename_current_branch(new_branch_name, force): # type: (Text, bool) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: git("checkout -b {}".format(new_branch_name)) flag = "-D" if force else "-d" git("branch {} {}".format(flag, current_branch)) tracker.rename_branch(current_branch, new_branch_name)
def make_child_branch(new_branch_name): # type: (str) -> None current_branch = get_current_branch() current_rev = hash_for(current_branch) with get_branch_tracker() as tracker: # Add the child using the current branch as the parent. tracker.add_child_for_parent(current_branch, new_branch_name, current_rev) # Make the new branch, also where the current branch is git("checkout -b %s" % new_branch_name)
def do_rebase(tracker, parent, child): # type: (BranchTracker, Text, Text) -> None base = tracker.base_for_branch(child) parent_rev = hash_for(parent) if base == parent_rev: return tracker.start_rebase(child, parent_rev) git("rebase --onto {} {} {}".format(parent, base, child)) tracker.finish_rebase(child, parent_rev)
def clean_invalid_branches(dry_run, archive, upstream): # type: (bool, bool, bool) -> None if upstream: # Make sure we have latest remote info git("fetch --prune") with get_branch_tracker() as tracker: for branch in tracker.linearized_branches(): if _is_branch_invalid(tracker, branch, upstream): if archive: _archive_invalid_branch(dry_run, tracker, branch) else: _delete_invalid_branch_if_possible(dry_run, tracker, branch, upstream)
def main(force_remove): # type: (bool) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: parent = tracker.parent_for_child(current_branch) children = tracker.children_for_parent(current_branch) assert not children, "Child branch should not have any children, found %s child(ren)" % len(children) git("checkout %s" % parent) # This will fail if we're not forcing the remove and the branch isn't merged in. delete_flag = "-D" if force_remove else "-d" git("branch %s %s" % (delete_flag, current_branch)) tracker.remove_child_leaf(current_branch)
def _test_clean_branches(target_directory): # type: (Text) -> None with _run_test(target_directory): _initialize_repo() # Create all the branches make_child_branch("valid_branch") assert get_current_branch() == "valid_branch" git("checkout master") make_child_branch("ghost_branch_childless") assert get_current_branch() == "ghost_branch_childless" git("checkout master") make_child_branch("ghost_branch_with_children") assert get_current_branch() == "ghost_branch_with_children" make_child_branch("child_of_ghost_branch") assert get_current_branch() == "child_of_ghost_branch" git("checkout master") print("Deleting branches from git") git("branch -D ghost_branch_childless") git("branch -D ghost_branch_with_children") print("Test cleaning by archiving") clean_invalid_branches(dry_run=False, archive=True, upstream=False) with get_branch_tracker() as tracker: assert not tracker.is_archived("valid_branch") assert tracker.is_archived("ghost_branch_childless") assert tracker.is_archived("ghost_branch_with_children") print("Clear archived flags for next test") tracker.set_is_archived("ghost_branch_childless", False) tracker.set_is_archived("ghost_branch_with_children", False) print("Test cleaning by deleting") clean_invalid_branches(dry_run=False, archive=False, upstream=False) with get_branch_tracker() as tracker: assert tracker.is_branch_tracked("valid_branch") assert not tracker.is_branch_tracked("ghost_branch_childless") assert tracker.is_branch_tracked("ghost_branch_with_children")
def rebase_children(is_recursive): # type: (bool) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: do_rebase(tracker, tracker.parent_for_child(current_branch), current_branch) if is_recursive: to_rebase_onto = [current_branch] while to_rebase_onto: parent = to_rebase_onto.pop() children = tracker.children_for_parent(parent) for child in children: do_rebase(tracker, parent, child) to_rebase_onto.append(child) # Go back to where we started. git("checkout {}".format(current_branch))
def rebase_children(is_recursive): # type: (bool) -> None current_branch = get_current_branch() with get_branch_tracker() as tracker: do_rebase(tracker, tracker.parent_for_child(current_branch), current_branch) if is_recursive: to_rebase_onto = [current_branch] while to_rebase_onto: parent = to_rebase_onto.pop() children = tracker.children_for_parent(parent) for child in children: do_rebase(tracker, parent, child) to_rebase_onto.append(child) # Go back to where we started. git("checkout %s" % current_branch)
def remove_branch(force_remove): # type: (bool) -> None current_branch = get_current_branch() current_commit = hash_for(current_branch) with get_branch_tracker() as tracker: parent = tracker.parent_for_child(current_branch) children = tracker.children_for_parent(current_branch) assert not children, \ "Child branch should not have any children, found {} child(ren)".format(len(children)) merged_into_parent = [ line[2:] for line in git("branch --merged {}".format(parent)).split('\n') if line ] if current_branch in merged_into_parent: print("Removing merged branch {!r} (was at commit {})".format( current_branch, current_commit)) elif force_remove: print("Force removing unmerged branch {!r} (was at commit {})". format( current_branch, current_commit, )) else: print("") print("!!!!!!!!") print("!!! Trying to remove branch {!r} not merged into its parent. Re-run with" \ "".format(current_branch)) print( "!!! '--force' if you want to force the deletion of this branch." ) print("!!!") print("!!! WARNING: Running with '--force' may cause data loss") print("!!!!!!!!") print("") exit(1) git("checkout {}".format(parent)) # This will fail if we're not forcing the remove and the branch isn't merged in. delete_flag = "-D" if force_remove else "-d" git("branch {} {}".format(delete_flag, current_branch)) tracker.remove_child_leaf(current_branch)
def make_child_branch(new_branch_name, revision=None): # type: (Text, Optional[Text]) -> None parent = get_current_branch() if revision is None: # Use the current revision as the base base_rev = hash_for("HEAD") else: # Use the merge-base of the given revision and the parent branch as the base base_rev = git("merge-base {} {}".format(parent, revision)).strip() with get_branch_tracker() as tracker: # Add the child using the current branch as the parent. tracker.add_child_for_parent(parent, new_branch_name, base_rev) # Make the new branch, either where the current branch is or at the specified revision if revision is None: command = "checkout -b {}".format(new_branch_name) else: command = "checkout -b {} {}".format(new_branch_name, revision) git(command)
def _initialize_repo(): # type: () -> None """ Initialize a repo and add a first commit so we can tell what branch we're on. """ print("Initializing repo") git("init") open("hello.txt", "w").close() git("add .") git("commit -am initial_commit") assert get_current_branch() == "master"
def _unit_tests(target_directory): # type: (str) -> None target_directory = os.path.expanduser(target_directory) target_container = os.path.dirname(target_directory) assert not os.path.exists(target_directory) assert os.path.isdir(target_container) with run_test(target_directory): # Initialize a repo and add a first commit so we can tell what branch we're on. print "Initializing repo" git("init") open("hello.txt", "w").close() git("add .") git("commit -am initial_commit") assert get_current_branch() == "master" original_commit = hash_for("HEAD") # Create all the branches make_child_branch("first_branch") assert get_current_branch() == "first_branch" make_child_branch("second_branch") assert get_current_branch() == "second_branch" make_child_branch("third_branch") assert get_current_branch() == "third_branch" # Rename a branch rename_current_branch("third_branch_renamed") assert get_current_branch() == "third_branch_renamed" # Rename it back rename_current_branch("third_branch") assert get_current_branch() == "third_branch" # This should be sibling to second_branch, on top of first_branch git("checkout first_branch") make_child_branch("sibling_branch") assert get_current_branch() == "sibling_branch" # Make the first "real" commit, in master print "First commit" git("checkout master") with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello!") # Avoiding spaces because of how we break up args in the `git` function git("commit -am first_commit_message") first_commit = hash_for("HEAD") assert original_commit != first_commit # Do the recursive rebase print "Rebase first_branch and its children on top of master." git("checkout first_branch") rebase_children(True) assert first_commit == hash_for("first_branch") assert first_commit == hash_for("second_branch") assert first_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") # Make a second commit, this time in first_branch print "Make a second commit in first_branch" git("checkout first_branch") with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello there!") git("commit -am second_commit_message") second_commit = hash_for("HEAD") assert original_commit != second_commit and first_commit != second_commit # Rebase just second_branch. This should update third_branch but shouldn't touch sibling_branch. print "Doing second rebase" git("checkout second_branch") rebase_children(True) assert second_commit == hash_for("first_branch") assert second_commit == hash_for("second_branch") assert second_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") print "Make a merge conflict in sibling_branch" git("checkout sibling_branch") with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello conflict") git("commit -am conflicting_change_message") # This should throw since the rebase has conflicts print "Testing merge conflicts" _assert_fails(lambda: rebase_children(True)) # Abort the rebase and try again git("rebase --abort") # It should fail for the same reason print "Testing merge conflicts again" _assert_fails(lambda: rebase_children(True)) print "Resolving the merge conflict" with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello merge") git("add hello.txt") git("rebase --continue") # This should effectively no-op print "Doing no-op rebase" current_commit = hash_for("HEAD") rebase_children(True) assert current_commit == hash_for("HEAD") assert get_branch_structure_string(False) == UNARCHIVED_PRINT_STRUCTURE set_archived(True, "second_branch") assert get_branch_structure_string(False) == ARCHIVED_PRINT_STRUCTURE assert get_branch_structure_string(True) == UNARCHIVED_PRINT_STRUCTURE git("checkout second_branch") set_archived(False) assert get_branch_structure_string(False) == UNARCHIVED_PRINT_STRUCTURE
def _integration_test(target_directory): # type: (Text) -> None with _run_test(target_directory): _initialize_repo() original_commit = hash_for("HEAD") # Create all the branches make_child_branch("first_branch") assert get_current_branch() == "first_branch" make_child_branch("second_branch") assert get_current_branch() == "second_branch" make_child_branch("third_branch") assert get_current_branch() == "third_branch" # Rename a branch rename_current_branch("third_branch_renamed", force=False) assert get_current_branch() == "third_branch_renamed" # Rename it back rename_current_branch("third_branch", force=False) assert get_current_branch() == "third_branch" # This should be sibling to second_branch, on top of first_branch git("checkout first_branch") make_child_branch("sibling_branch") assert get_current_branch() == "sibling_branch" # Make the first "real" commit, in master print("First commit") git("checkout master") with open(os.path.join(target_directory, "hello.txt"), "wb") as f: f.write(b"Hello!") # Avoiding spaces because of how we break up args in the `git` function git("commit -am first_commit_message") first_commit = hash_for("HEAD") assert original_commit != first_commit # Do the recursive rebase print("Rebase first_branch and its children on top of master.") git("checkout first_branch") rebase_children(True) assert first_commit == hash_for("first_branch") assert first_commit == hash_for("second_branch") assert first_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") # Make a second commit, this time in first_branch print("Make a second commit in first_branch") git("checkout first_branch") with open(os.path.join(target_directory, "hello.txt"), "wb") as f: f.write(b"Hello there!") git("commit -am second_commit_message") second_commit = hash_for("HEAD") assert original_commit != second_commit and first_commit != second_commit # Rebase just second_branch. This should update third_branch but shouldn't touch # sibling_branch. print("Doing second rebase") git("checkout second_branch") rebase_children(True) assert second_commit == hash_for("first_branch") assert second_commit == hash_for("second_branch") assert second_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") print("Make a merge conflict in sibling_branch") # Add a new commit to the sibling branch, delete the branch, and re-create it at the # revision it was at. git("checkout sibling_branch") # Make a conflicting change on sibling so that we can test rebasing it later. with open(os.path.join(target_directory, "hello.txt"), "wb") as f: f.write(b"Hello conflict") git("commit -am conflicting_change_message") sibling_conflicting_commit = hash_for("HEAD") print("Test deleting branch") # See that removing fails since it's not merged _assert_fails(lambda: _command_with_args(GitRemoveLeafBranch, [])) assert get_current_branch() == "sibling_branch" assert sibling_conflicting_commit == hash_for("HEAD") _command_with_args(GitRemoveLeafBranch, ["--force"]) assert get_current_branch() == "first_branch" assert second_commit == hash_for("HEAD") print("Test creating branch at specific revision") make_child_branch("sibling_branch", sibling_conflicting_commit) assert get_current_branch() == "sibling_branch" assert sibling_conflicting_commit == hash_for("HEAD") # This should throw since the rebase has conflicts print("Testing merge conflicts") _assert_fails(lambda: rebase_children(True)) # Abort the rebase and try again git("rebase --abort") # It should fail for the same reason print("Testing merge conflicts again") _assert_fails(lambda: rebase_children(True)) print("Resolving the merge conflict") with open(os.path.join(target_directory, "hello.txt"), "wb") as f: f.write(b"Hello merge") git("add hello.txt") git("rebase --continue") # This should effectively no-op print("Doing no-op rebase") current_commit = hash_for("HEAD") rebase_children(True) assert current_commit == hash_for("HEAD") unarchived_print_structure_sibling_branch = UNARCHIVED_PRINT_STRUCTURE.replace( "sibling_branch", make_green("sibling_branch") ) archived_print_structure_sibling_branch = ARCHIVED_PRINT_STRUCTURE.replace( "sibling_branch", make_green("sibling_branch") ) assert get_branch_structure_string(False) == unarchived_print_structure_sibling_branch set_archived(True, "second_branch") assert get_branch_structure_string(False) == archived_print_structure_sibling_branch assert get_branch_structure_string(True) == unarchived_print_structure_sibling_branch git("checkout second_branch") set_archived(False) assert get_branch_structure_string(False) == UNARCHIVED_PRINT_STRUCTURE.replace( "second_branch", make_green("second_branch") ) current_commit = hash_for("HEAD") assert get_branch_info( branch=None, use_null_delimiter=False ) == "Parent branch: first_branch; Base revision: {}".format(current_commit) assert get_branch_info(branch=None, use_null_delimiter=True) == "first_branch\0{}".format(current_commit) try: get_branch_info(branch="master", use_null_delimiter=False) assert False, "Should not get here" except SystemExit as e: assert str(e) == "Branch does not have a parent: master"
def _integration_test(target_directory): # type: (str) -> None target_directory = os.path.expanduser(target_directory) target_container = os.path.dirname(target_directory) assert not os.path.exists(target_directory) assert os.path.isdir(target_container) with run_test(target_directory): # Initialize a repo and add a first commit so we can tell what branch we're on. print "Initializing repo" git("init") open("hello.txt", "w").close() git("add .") git("commit -am initial_commit") assert get_current_branch() == "master" original_commit = hash_for("HEAD") # Create all the branches make_child_branch("first_branch") assert get_current_branch() == "first_branch" make_child_branch("second_branch") assert get_current_branch() == "second_branch" make_child_branch("third_branch") assert get_current_branch() == "third_branch" # Rename a branch rename_current_branch("third_branch_renamed") assert get_current_branch() == "third_branch_renamed" # Rename it back rename_current_branch("third_branch") assert get_current_branch() == "third_branch" # This should be sibling to second_branch, on top of first_branch git("checkout first_branch") make_child_branch("sibling_branch") assert get_current_branch() == "sibling_branch" # Make the first "real" commit, in master print "First commit" git("checkout master") with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello!") # Avoiding spaces because of how we break up args in the `git` function git("commit -am first_commit_message") first_commit = hash_for("HEAD") assert original_commit != first_commit # Do the recursive rebase print "Rebase first_branch and its children on top of master." git("checkout first_branch") rebase_children(True) assert first_commit == hash_for("first_branch") assert first_commit == hash_for("second_branch") assert first_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") # Make a second commit, this time in first_branch print "Make a second commit in first_branch" git("checkout first_branch") with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello there!") git("commit -am second_commit_message") second_commit = hash_for("HEAD") assert original_commit != second_commit and first_commit != second_commit # Rebase just second_branch. This should update third_branch but shouldn't touch # sibling_branch. print "Doing second rebase" git("checkout second_branch") rebase_children(True) assert second_commit == hash_for("first_branch") assert second_commit == hash_for("second_branch") assert second_commit == hash_for("third_branch") assert first_commit == hash_for("sibling_branch") print "Make a merge conflict in sibling_branch" # Add a new commit to the sibling branch, delete the branch, and re-create it at the # revision it was at. git("checkout sibling_branch") # Make a conflicting change on sibling so that we can test rebasing it later. with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello conflict") git("commit -am conflicting_change_message") sibling_conflicting_commit = hash_for('HEAD') print "Test deleting branch" # See that removing fails since it's not merged _assert_fails(lambda: remove_branch(force_remove=False)) assert get_current_branch() == 'sibling_branch' assert sibling_conflicting_commit == hash_for('HEAD') remove_branch(force_remove=True) assert get_current_branch() == 'first_branch' assert second_commit == hash_for('HEAD') print "Test creating branch at specific revision" make_child_branch('sibling_branch', sibling_conflicting_commit) assert get_current_branch() == 'sibling_branch' assert sibling_conflicting_commit == hash_for('HEAD') # This should throw since the rebase has conflicts print "Testing merge conflicts" _assert_fails(lambda: rebase_children(True)) # Abort the rebase and try again git("rebase --abort") # It should fail for the same reason print "Testing merge conflicts again" _assert_fails(lambda: rebase_children(True)) print "Resolving the merge conflict" with open(os.path.join(target_directory, "hello.txt"), "w") as f: f.write("Hello merge") git("add hello.txt") git("rebase --continue") # This should effectively no-op print "Doing no-op rebase" current_commit = hash_for("HEAD") rebase_children(True) assert current_commit == hash_for("HEAD") assert get_branch_structure_string(False) == UNARCHIVED_PRINT_STRUCTURE set_archived(True, "second_branch") assert get_branch_structure_string(False) == ARCHIVED_PRINT_STRUCTURE assert get_branch_structure_string(True) == UNARCHIVED_PRINT_STRUCTURE git("checkout second_branch") set_archived(False) assert get_branch_structure_string(False) == UNARCHIVED_PRINT_STRUCTURE