From 02d3a978c86b890759e4c10d3c13a40fd6a4d709 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Tue, 27 Nov 2007 20:22:30 +0000 Subject: [PATCH] Put release commits on a new branch until published. We used to write two commits to version control during release (one with the release version, and one to start the new snapshot series). These would get reverted if the used failed the release. The problem was that when bugs were found it was tempting to make (and possibly commit) changes to this modified version. These changes then had to be rebased to the point before the release. Now, the two commits are made on a new temporary branch. HEAD remains at its original location during release testing, so commits are against the right version and go to the right place. Failing a build now only requires changing the temporary branch. Publishing moves HEAD to the tip of the new branch. --- release.py | 37 ++++++++++++++++++++++--------------- scm.py | 35 +++++++++++++++++++++++++---------- support.py | 27 +++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/release.py b/release.py index 557478d..b28d4f4 100644 --- a/release.py +++ b/release.py @@ -15,6 +15,8 @@ release_status_file = 'release-status' valid_phases = ['commit-release', 'generate-archive'] +TMP_BRANCH_NAME = '0release-tmp' + def run_unit_tests(impl): self_test = impl.metadata.get('self-test', None) if self_test is None: @@ -117,8 +119,7 @@ def do_release(local_iface, options): status.old_snapshot_version = local_impl.get_version() status.release_version = release_version - scm.commit('Release %s' % release_version) - status.head_at_release = scm.get_head_revision() + status.head_at_release = scm.commit('Release %s' % release_version, branch = TMP_BRANCH_NAME, parent = 'HEAD') status.save() return release_version @@ -126,7 +127,7 @@ def do_release(local_iface, options): def set_to_snapshot(snapshot_version): assert snapshot_version.endswith('-post') support.publish(local_iface.uri, set_released = '', set_version = snapshot_version) - scm.commit('Start development series %s' % snapshot_version) + scm.commit('Start development series %s' % snapshot_version, branch = TMP_BRANCH_NAME, parent = TMP_BRANCH_NAME) status.new_snapshot_version = scm.get_head_revision() status.save() @@ -176,12 +177,7 @@ def do_release(local_iface, options): def fail_candidate(archive_file): support.backup_if_exists(archive_file) - head = scm.get_head_revision() - if head != status.new_snapshot_version: - raise SafeException("There have been commits since starting the release! Please rebase them onto %s" % status.head_before_release) - # Check no uncommitted changes - scm.ensure_committed() - scm.reset_hard(status.head_before_release) + scm.delete_branch(TMP_BRANCH_NAME) os.unlink(release_status_file) print "Restored to state before starting release. Make your fixes and try again..." @@ -191,6 +187,8 @@ def do_release(local_iface, options): if status.tagged: print "Already tagged and added to master feed." else: + scm.ensure_committed() + tar = tarfile.open(archive_file, 'r:bz2') stream = tar.extractfile(tar.getmember(archive_name + '/' + local_iface_rel_path)) remote_dl_iface = create_feed(stream, archive_file, archive_name, version) @@ -200,6 +198,8 @@ def do_release(local_iface, options): remote_dl_iface.close() scm.tag(status.release_version, status.head_at_release) + scm.reset_hard(TMP_BRANCH_NAME) + scm.delete_branch(TMP_BRANCH_NAME) status.tagged = 'true' status.save() @@ -234,17 +234,17 @@ def do_release(local_iface, options): if status.head_before_release: head = scm.get_head_revision() if status.release_version: - print "RESUMING release of version %s" % status.release_version + print "RESUMING release of %s %s" % (local_iface.get_name(), status.release_version) elif head == status.head_before_release: - print "Restarting release (HEAD revision has not changed)" + print "Restarting release of %s (HEAD revision has not changed)" % local_iface.get_name() else: raise SafeException("Something went wrong with the last run:\n" + "HEAD revision for last run was " + status.head_before_release + "\n" + "HEAD revision now is " + head + "\n" + "You should revert your working copy to the previous head and try again.\n" + "If you're sure you want to release from the current head, delete '" + release_status_file + "'") - - print "Releasing", local_iface.get_name() + else: + print "Releasing", local_iface.get_name() ensure_ready_to_release() @@ -253,8 +253,12 @@ def do_release(local_iface, options): need_set_snapshot = False if status.new_snapshot_version: head = scm.get_head_revision() - if head != status.new_snapshot_version: - print "WARNING: there are more commits since we tagged; they will not be included in the release!" + if head != status.head_before_release: + raise SafeException("There are more commits since we started!\n" + "HEAD was " + status.head_before_release + "\n" + "HEAD now " + head + "\n" + "To include them, delete '" + release_status_file + "' and try again.\n" + "To leave them out, put them on a new branch and reset HEAD to the release version.") else: raise SafeException("Something went wrong previously when setting the new snapshot version.\n" + "Suggest you reset to the original HEAD of\n%s and delete '%s'." % (status.head_before_release, release_status_file)) @@ -286,6 +290,9 @@ def do_release(local_iface, options): if need_set_snapshot: set_to_snapshot(version + '-post') + # Revert back to the original revision, so that any fixes the user makes + # will get applied before the tag + scm.reset_hard(scm.get_current_branch()) #backup_if_exists(archive_name) unpack_tarball(archive_file, archive_name) diff --git a/scm.py b/scm.py index aba56a6..50149d1 100644 --- a/scm.py +++ b/scm.py @@ -21,6 +21,13 @@ class GIT(SCM): if code: raise SafeException("Git %s failed with exit code %d" % (repr(args), code)) + def _run_stdout(self, args, **kwargs): + child = self._run(args, stdout = subprocess.PIPE, **kwargs) + stdout, unused = child.communicate() + if child.returncode: + raise SafeException('Failed to get current branch! Exit code %d: %s' % (child.returncode, stdout)) + return stdout + def reset_hard(self, revision): self._run_check(['reset', '--hard', revision]) @@ -42,15 +49,16 @@ class GIT(SCM): self._run_check(['tag', '-s'] + key_opts + ['-m', 'Release %s' % version, tag, revision]) print "Tagged as %s" % tag - def push_head_and_release(self, version): - child = self._run(['symbolic-ref', 'HEAD'], stdout = subprocess.PIPE) - stdout, unused = child.communicate() - if child.returncode: - print stdout - raise SafeException('Failed to get current branch! Exit code %d' % child.returncode) - current_branch = stdout.strip() + def get_current_branch(self): + current_branch = self._run_stdout(['symbolic-ref', 'HEAD']).strip() info("Current branch is %s", current_branch) - self._run_check(['push', self.options.public_scm_repository, self.make_tag(version), current_branch]) + return current_branch + + def delete_branch(self, branch): + self._run_check(['branch', '-D', branch]) + + def push_head_and_release(self, version): + self._run_check(['push', self.options.public_scm_repository, self.make_tag(version), self.get_current_branch()]) def ensure_no_tag(self, version): tag = self.make_tag(version) @@ -69,8 +77,15 @@ class GIT(SCM): os.unlink(archive_file) raise SafeException("git-archive failed with exit code %d" % status) - def commit(self, message): - self._run_check(['commit', '-q', '-a', '-m', message]) + def commit(self, message, branch, parent): + self._run_check(['add', '-u']) # Commit all changed tracked files to index + tree = self._run_stdout(['write-tree']).strip() + child = self._run(['commit-tree', tree, '-p', parent], stdin = subprocess.PIPE, stdout = subprocess.PIPE) + stdout, unused = child.communicate(message) + commit = stdout.strip() + info("Committed as %s", commit) + self._run_check(['branch', '-f', branch, commit]) + return commit def get_head_revision(self): proc = self._run(['rev-parse', 'HEAD'], stdout = subprocess.PIPE) diff --git a/support.py b/support.py index 929795f..dbbba03 100644 --- a/support.py +++ b/support.py @@ -75,3 +75,30 @@ def get_choice(*options): if o.lower().startswith(choice): return o +class Status(object): + __slots__ = ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version', 'head_at_release', 'created_archive', 'tagged'] + def __init__(self): + for name in self.__slots__: + setattr(self, name, None) + + if os.path.isfile(release_status_file): + for line in file(release_status_file): + assert line.endswith('\n') + line = line[:-1] + name, value = line.split('=') + setattr(self, name, value) + info("Loaded status %s=%s", name, value) + + def save(self): + tmp_name = release_status_file + '.new' + tmp = file(tmp_name, 'w') + try: + lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)] + tmp.write(''.join(lines)) + tmp.close() + os.rename(tmp_name, release_status_file) + info("Wrote status to %s", release_status_file) + except: + os.unlink(tmp_name) + raise + -- 2.11.4.GIT