diff options
author | Ryan S. Arnold <ryan.arnold@linaro.org> | 2016-08-20 01:04:53 -0500 |
---|---|---|
committer | Ryan S. Arnold <ryan.arnold@linaro.org> | 2016-08-20 01:04:53 -0500 |
commit | 737c797232f444acee6462df4deb41ad06be7021 (patch) | |
tree | a2297998b694298f167d14724077cbaabfafc508 | |
parent | 9dac4d48397f5f805e78523f2ee2214b1f09facf (diff) |
Add support for generating 'release' release notes using tags.
This required quite a bit of refactoring.
Added series_from_tag functions and corresponding tests, as
well as series_from_branch input tests that test input
conformance.
Removed unnecessary CandidateRN, SnapshotRN, and ReleaseRN
classes to shallow the topology.
-rw-r--r-- | linaropy/git/clone.py | 11 | ||||
-rw-r--r-- | linaropy/git/gitrepo.py | 37 | ||||
-rw-r--r-- | linaropy/rn/gccclone.py | 2 | ||||
-rw-r--r-- | linaropy/rn/linaroseries.py | 14 | ||||
-rw-r--r-- | linaropy/rn/rngen.py | 31 | ||||
-rw-r--r-- | linaropy/rn/rnseries.py | 300 | ||||
-rw-r--r-- | linaropy/rn/template.py | 56 | ||||
-rw-r--r-- | linaropy/series.py | 205 | ||||
-rw-r--r-- | rn.py | 90 | ||||
-rwxr-xr-x | tcwg-release.sh | 4 |
10 files changed, 483 insertions, 267 deletions
diff --git a/linaropy/git/clone.py b/linaropy/git/clone.py index d8fe126..2fb3843 100644 --- a/linaropy/git/clone.py +++ b/linaropy/git/clone.py @@ -63,7 +63,9 @@ class Clone(GitRepo): except ErrorReturnCode: raise EnvironmentError('Specified clonedir is not a git repository.') - # Strip off the directory name + # Strip off any trailing / + clonedir=clonedir.rstrip('/') + # We only want the final directory name, not the whole path. clone_dirname=clonedir.rsplit('/', 1)[-1] self.repodir=self.proj.projdir + '/' + clone_dirname @@ -180,6 +182,13 @@ class TestClone(unittest.TestCase): # Verify that a symlink is used for the clone. self.assertTrue(os.path.islink(self.clone.clonedir())) + # This was a bug at some point, as it would strip off the repository name. + def test_existing_clonedir_with_trailing_slash(self): + self.clone=Clone(self.proj,clonedir=TestClone.rnclone.clonedir() + "/") + + # Verify that a symlink is used for the clone. + self.assertTrue(os.path.islink(self.clone.clonedir())) + def test_failed_symlink(self): # We'll make the projdir read-only so we can force symlink creation # to fail. diff --git a/linaropy/git/gitrepo.py b/linaropy/git/gitrepo.py index 99b2573..b9f2c03 100644 --- a/linaropy/git/gitrepo.py +++ b/linaropy/git/gitrepo.py @@ -45,6 +45,7 @@ class GitRepo(object): raise TypeError('proj input parameter is not of type Proj') def branchexists(self, branch): + logging.info("Checking to see if branch %s exists" % branch) with cd(self.repodir): try: # Quote the branchname because it might have strange @@ -52,6 +53,7 @@ class GitRepo(object): br="%s" % branch git("rev-parse", "--verify", br) except ErrorReturnCode_128: + logging.info("Couldn't find branch %s" % branch) return False return True @@ -63,6 +65,11 @@ class GitRepo(object): raise EnvironmentError("Unable to get the current branch") return branch + # TODO make this a bit more sophisticated because the user might not be + # using 'origin' as their remote name. + def remote_branchname(self, branchname): + return "remotes/origin/" + branchname + # TODO: Fully test this. # Stash current changes and change to new branch. # Return to previous branch when context is exited. @@ -125,18 +132,23 @@ class GitRepo(object): # TODO: Write a unit test for this. # TODO: fix the default def commit(self, message="default"): - print "Committing changes." + logging.info("Attempting to commit changes to %s" % self.repodir ) try: with cd(self.repodir): # Git commit first with a boiler plate message and then allow the user # to amend. if git("status", "--porcelain"): - # print git("commit", "-m", '%s' % message, _err_to_out=True) + # using python sh will suppress the git editor subprocess.call(["git", "commit", "-m", message]) - # using python sh will suppress the git editor subprocess.call(["git", "commit", "--amend"]) + else: + logging.info("Nothing to commit.") + return False except ErrorReturnCode: raise EnvironmentError("Unable to git commit ") + return False + # Something was committed. + return True def log(self, number): try: @@ -146,7 +158,7 @@ class GitRepo(object): raise EnvironmentError("Unable to git add " + filetogitadd) # TODO: Does this need to 'cd' first? - def editFile(self, toedit): + def edit(self, toedit): editor = os.getenv('EDITOR') if not editor: editor='/usr/bin/vim' @@ -169,6 +181,17 @@ class GitRepo(object): except ErrorReturnCode: raise EnvironmentError("Unable to push branch %s to %s" % (branch, tobranch)) + def tag_exists(self, tag): + try: + with cd(self.repodir): + print("testing tag %s on repo %s" % (tag, self.repodir)) + tagref="refs/tags/%s" % tag + git("rev-parse", "-q", "--verify", tagref) + except ErrorReturnCode_128: + logging.info("Couldn't find tag %s" % tag) + return False + return True + def __str__(self): return self.repodir @@ -212,8 +235,10 @@ class TestGitRepo(unittest.TestCase): # TODO: Create a test-branch in the repo so it's always there. - def test_branchexists(self): - self.assertTrue(self.dgr.branchexists("remotes/origin/releases/linaro-4.9-2015.05")) + def test_tag_exists(self): + with cd(self.dgr.repodir): + git("tag", "-a", "linaro-99.9-2099.08-rc1", "-m", "This is a test tag") + self.assertTrue(self.dgr.tag_exists("linaro-99.9-2099.08-rc1")) def test_not_branchexists(self): self.assertFalse(self.dgr.branchexists("foobar")) diff --git a/linaropy/rn/gccclone.py b/linaropy/rn/gccclone.py index 36f610b..c6582f3 100644 --- a/linaropy/rn/gccclone.py +++ b/linaropy/rn/gccclone.py @@ -1,3 +1,4 @@ +import logging from sh import git from ..git.clone import Clone @@ -7,6 +8,7 @@ class GCCClone(Clone): def __init__(self, proj, clonedir=None, remote=None ): super(GCCClone,self).__init__(proj, clonedir, remote) + logging.info("Changing directory to clonedir %s." % clonedir) with cd(clonedir): try: with open('gcc/BASE-VER', 'r') as f: diff --git a/linaropy/rn/linaroseries.py b/linaropy/rn/linaroseries.py index f978655..b3e0322 100644 --- a/linaropy/rn/linaroseries.py +++ b/linaropy/rn/linaroseries.py @@ -1,12 +1,14 @@ import unittest import copy +import logging from datetime import datetime from dateutil.relativedelta import relativedelta from ..vers import Spin from ..vers import Rc from ..series import Series -from ..series import seriesFromBranchname +from ..series import series_from_branchname +from ..series import series_from_tag # Progression: # The Linaro release progression follows. @@ -50,6 +52,7 @@ class LinaroSeries(Series): super(LinaroSeries,self).__init__(seriestype, vendor, package, date,spin,rc, strict) def toNextCandidate(self, date=None, strict=False): + logging.info('from %s toNextCandidate' % self.longlowertype()) if self.seriestype < 0 or self.seriestype >= len(Series.series): raise TypeError('toNextCandidate on an unknown series type.') @@ -91,6 +94,7 @@ class LinaroSeries(Series): return candidate def toNextRelease(self, date=None, strict=False): + logging.info('from %s toNextRelease' % self.longlowertype()) if self.seriestype < 0 or self.seriestype >= len(Series.series): raise TypeError('toNextRelease called on an unknown series type.') elif self.seriestype == Series.series.index("release"): @@ -116,6 +120,7 @@ class LinaroSeries(Series): return release def toNextSnapshot(self, date=None, strict=False): + logging.info('from %s toNextSnapshot' % self.longlowertype()) if self.seriestype < 0 or self.seriestype >= len(Series.series): raise TypeError('toNextRelease called on an unknown series type.') elif self.seriestype == Series.series.index("candidate"): @@ -146,6 +151,11 @@ class LinaroSeries(Series): def toNext(self, seriestype, date=None, strict=False): return self.dispatchnext[seriestype](date, strict) +def linaro_series_from_tag(tag=None): + series=series_from_tag(tag) + linaroseries=LinaroSeries(Series.series[series.seriestype], vendor="linaro", package=series.package ,date=series.date , spin=series.spin, rc=series.rc, strict=True ) + return linaroseries + # Helper function which creates a LinaroSeries from a properly formed branch name # input string. def linaroSeriesFromBranchname(branch=None): @@ -153,7 +163,7 @@ def linaroSeriesFromBranchname(branch=None): # Create a Series using the helper function and then populate a # LinaroSeries. TODO: There's probably a better way to do this such # as a SeriesFactory that is overridden. - series=seriesFromBranchname(branch) + series=series_from_branchname(branch) linaroseries=LinaroSeries(Series.series[series.seriestype], vendor="linaro", package=series.package ,date=series.date , spin=series.spin, rc=series.rc, strict=True ) return linaroseries diff --git a/linaropy/rn/rngen.py b/linaropy/rn/rngen.py index d418712..bfbb05b 100644 --- a/linaropy/rn/rngen.py +++ b/linaropy/rn/rngen.py @@ -32,7 +32,7 @@ import urllib2 from ..series import Series from linaroseries import LinaroSeries -from rnseries import CandidateRN +from rnseries import RNSeries from gccclone import GCCClone from ..rninput import finput @@ -42,11 +42,9 @@ def get_gcc_version(gccclone): (dct["major"],dct["minor"],dct["revision"]) = gccclone.get_base_version().split(".") return dct -def rngen(candidate, gccclone): - #TODO Test that candidate is a CandidateRN - - trackseries=candidate.get_track_series() - nextseries=candidate.get_next_series() +def rngen(rn_series, gccclone): + trackseries=rn_series.track_series + nextseries=rn_series.next_series print "toNext: " + nextseries.getBranchname() print "toNext: " + format(nextseries, '%N/%d') @@ -80,7 +78,7 @@ def rngen(candidate, gccclone): gcc_version_path=ver['major'] stab_maint='Stable' - print "This is a %s candidate for GCC version %s" % (stab_maint, + print "This is a %s RNSeries for GCC version %s" % (stab_maint, gcc_version_path) history=finput('Please enter the location of the changelog csv file: ', "6.1-2016.06.csv") @@ -113,8 +111,8 @@ def rngen(candidate, gccclone): spin=str(nextseries.spin).strip('-') rc=str(nextseries.rc).strip("-rc") - template_dir=candidate.rntemplate.workdir() - series_dir=candidate.rnworkdir.workdir() + template_dir=rn_series.rn_template.workdir() + series_dir=rn_series.rn_series.workdir() # Verify that the basis url is actually valid, otherwise the release notes # will contain erroneous information. @@ -290,14 +288,7 @@ def rngen(candidate, gccclone): copyfile(out_source + u'.textile' ,src_readme ) print 'Wrote generated file ' + out_source + u'.textile' + " to " + src_readme - candidate.rnworkdir.add(bin_readme) - candidate.rnworkdir.add(src_readme) - candidate.rnworkdir.commit("Generated Release Notes for " + nextseries.label()) - candidate.rnworkdir.log(1) - - # TODO: Ask if the user would like to push the release notes changes. - - quit(0); - -if __name__ == "__main__": - main(sys.argv[1:]) + rn_series.rn_series.add(bin_readme) + rn_series.rn_series.add(src_readme) + rn_series.rn_series.commit("Generated Release Notes for %s." % nextseries.label()) + rn_series.rn_series.log(1) diff --git a/linaropy/rn/rnseries.py b/linaropy/rn/rnseries.py index d3e2c3f..284e6d9 100644 --- a/linaropy/rn/rnseries.py +++ b/linaropy/rn/rnseries.py @@ -10,10 +10,9 @@ from ..git.gitrepo import cd from ..git.clone import Clone from ..git.workdir import Workdir from ..series import Series -from ..series import seriesFromBranchname from linaroseries import LinaroSeries from linaroseries import linaroSeriesFromBranchname -from template import TemplateRN +from template import RNTemplate from ..rninput import yninput @@ -27,10 +26,10 @@ class RNSeries(object): _series='components/toolchain/binaries/README.textile.series' # @rnrepo - path to the existing releases notes repository if there is one. # If there isn't one a new one will be cloned. - # @trackseries - an instance of a LinaroSeries to track. - # @nextseries - an instance of a LinaroSeries that is the next series. - def __init__(self, proj, rnrepo=None, trackseries=None, nextseries=None, headless=False): - + # @track_series - an instance of a LinaroSeries to track. + # @next_series - an instance of a LinaroSeries that is the next series. + def __init__(self, proj, rnrepo=None, track_series=None, next_series=None, headless=False): + self.proj=proj # Create the release-notes repository clone. The Clone constructor # will throw an exception if proj is not a Proj. That is an # unrecoverable error so don't catch it here. The Clone constructor @@ -38,156 +37,167 @@ class RNSeries(object): if rnrepo: logging.info('RNSeries() clone already exists. Using existing.') - self.rnclone=Clone(proj, clonedir=rnrepo) + self.rn_clone=Clone(proj, clonedir=rnrepo) else: - self.rnclone=Clone(proj, remote=RNSeries.rnremote) + self.rn_clone=Clone(proj, remote=RNSeries.rnremote) # TODO: Write a testcase that exposes this. - if not trackseries: - raise TypeError("Input variable trackseries is required.") + if not track_series: + raise TypeError("Input variable track_series is required.") # TODO: Write a testcase that exposes this. - if not isinstance(trackseries, LinaroSeries): - raise TypeError("Input variable trackseries not of type Series.") + if not isinstance(track_series, LinaroSeries): + raise TypeError("Input variable track_series not of type Series.") # TODO: Write a testcase that exposes this. - if not nextseries: - raise TypeError("Input variable nextseries is required.") + if not next_series: + raise TypeError("Input variable next_series is required.") # TODO: Write a testcase that exposes this. - if not isinstance(nextseries, LinaroSeries): - raise TypeError("Input variable nextseries not of type Series.") - - self.trackseries=trackseries - self.nextseries=nextseries - - self.proj=proj - - templ_branchname=self.nextseries.shorttype() + "_" + self.nextseries.getDir() - - logging.info('RNSeries() Creating TemplateRN') - # Every release-notes series contains a 'template' workdir checked out - # into a new templ_branchname branch that tracks master. - self.rntemplate=TemplateRN(self.proj,self.rnclone, branchname_suffix=templ_branchname) - - # We do this now because the SeriesRN workdir might be branched from the template. - self.rntemplate.updateTemplateReadme() - updateseries=self.rntemplate.updateTemplateSeries() - - self.rntemplate.commit() - - print "Please verify that your changes have been committed on the template branch." - self.rntemplate.log(1) - - # It's possible that self.trackseries.getBranchname() doesn't exist - # (such is the case if we're creating the first candidate from a - # snapshot). In this case 'track' should be the local template branch - # so that we pick up the template changes. - if not self.rntemplate.branchexists(self.trackseries.getBranchname()): - print("Creating RNSeries based on branch " + self.rntemplate.getbranch()) - self.rnworkdir=Workdir(proj=self.proj, clone=self.rnclone, workdir=self.nextseries.shorttype(), track=self.rntemplate.getbranch(), branchname=self.nextseries.getBranchname()) - else: - print "You updated the template README.textile.series but this will not be reflected in the Candidate README.textile.series." - print "Creating RNSeries based on branch " + self.trackseries.getBranchname() - self.rnworkdir=Workdir(proj=self.proj, clone=self.rnclone, workdir=self.nextseries.shorttype(), track=self.trackseries.getBranchname(), branchname=self.nextseries.getBranchname()) - - print "Log of last commit on workdir branch:" - self.rnworkdir.log(1) - - print "If you would like to make changes to the series release notes please update the the Candidate README.textile.series file:" - answer=yninput("Do you need to update the Candidate README.textile.series file?", "N") - if answer == "y": - self.rnworkdir.editFile(self._series) - else: - print "The Candidate README.textile.series file has not been updated." - - self.rnworkdir.commit() - - print "Log of last commit on workdir branch:" - self.rnworkdir.log(1) - + if not isinstance(next_series, LinaroSeries): + raise TypeError("Input variable next_series not of type Series.") + + self.track_series=track_series + self.next_series=next_series + + # Default to None, as this is set in update_template_readmes + self.rn_template=None + self.rn_series=None + + def update_templ_readmes(self, headless=False): + """ + Prompt the user to update the Template README.textile files. + + The first time this is called it creates the Template repository. + + This must be called before update_rnseries_reamdes(). + + Parameters + ---------- + headless=False : bool + Automatically decline to edit any of the README.textile files. + This is useful for automated testing. If headless==True then + the template readmes will not be updated. Only the template + repository will be created. + """ + templ_branchname=self.next_series.shorttype() + templ_branchname + templ_branchname + "_" + self.next_series.getDir() + if not self.rn_template: + logging.info("Creating the RNTemplate instance.") + self.rn_template=RNTemplate( + self.proj, + self.rn_clone, + branchname_suffix=templ_branchname) + + logging.info( + "Requesting updates to the TemplateReadme and series files.") + + self.rn_template.update_readme(headless=headless) + self.rn_template.update_series(headless=headless) + + logging.info( + "Commiting README.textile and README.textile.series files to" + "template repository.") + + return self.rn_template.commit() + + def update_series_readmes(self, headless=False): + """ + Prompt the user to update the series README.textile.series file. + + The first time this is called it creates the series workdir + repository. + + The update_template_reamdes() function must be called before this + is called. + + Exceptions + ---------- + RuntimeError + Returned if update_template_readmes() hasn't been called yet. + + Parameters + ---------- + headless=False : bool + Automatically decline to edit any of the README.textile files. + This is useful for automated testing. If headless==True then + the template readmes will not be updated. Only the template + repository will be created. + + """ + # The RNTemplate instance is necessary as we might need to derive + # the SeriesRN from it. + if not self.rn_template: + raise RuntimeError( + "The rn_template doesn't yet exist. Call" + " update_template_readmes() first.") + + remote_track=self.rn_clone.remote_branchname(self.track_series.getBranchname()) + if not self.rn_series: + print("Looking for branch %s" % self.track_series.getBranchname()) + # The workdir for the first candidate series will be derived from + # the changes made to the master branch templates. The workdir for + # subsequent candidates and releases will be made based on the + # existing candidate or release branch. + #if not self.rn_clone.branchexists(self.track_series.getBranchname()): + if not self.rn_clone.branchexists(remote_track): + logging.info( + "Creating RNSeries based on branch " + + self.rn_template.getbranch()) + + self.rn_series=Workdir( + proj=self.proj, + clone=self.rn_clone, + workdir=self.next_series.shorttype(), + track=self.rn_template.getbranch(), + branchname=self.next_series.getBranchname()) + else: + logging.info( + "MARK Creating RNSeries based on branch " + + remote_track) + # + self.track_series.getBranchname()) + + self.rn_series=Workdir( + proj=self.proj, + clone=self.rn_clone, + workdir=self.next_series.shorttype(), + track=remote_track, + branchname=self.next_series.getBranchname()) + #track=self.track_series.getBranchname(), + + logging.warning( + "If you updated the template README.textile.series file" + " the changes will not be reflected in this series" + " README.textile.series as this series is derived from %s." + " Please make the changes to the series" + " README.textile.series file as well." % + remote_track) + # self.track_series.getBranchname()) + + # TODO: Ask if the user would like to merge the changes in + # the template README.textile.series file instead of the + # long-winded warning above. + + if not headless: + answer=yninput( + "Would you like to update the %s README.textile.series" + " file?" % self.next_series.longlowertype(), "N") + if answer == "y": + self.rn_series.edit(self._series) + + return self.rn_series.commit( + "Added NEWS items for %s." % self.next_series.getBranchname()) + + def push_readme_updates(self): + pass # TODO: Don't forget to push template and workdir changes. - # self.rntemplate.pushToBranch("origin/master") + # self.rn_template.pushToBranch("origin/master") # TODO: Should the workdir know where to push to? - # self.rnworkdir.pushToBranch(self.nextseries.getBranchname()) - - def get_next_series(self): - return self.nextseries - - def get_track_series(self): - return self.trackseries - -# Do we even need these, especially if they're all the same? -class SnapshotRN(RNSeries): - def __init__(self, proj, trackseries, rnrepo=None): - if not isinstance(trackseries, LinaroSeries): - raise TypeError('The series input must be of type LinaroSeries') - - nextseries=trackseries.toNextSnapshot(strict=True) - - super(SnapshotRN,self).__init__(proj, rnrepo, trackseries=trackseries, nextseries=nextseries) - -class CandidateRN(RNSeries): - def __init__(self, proj, trackseries, rnrepo=None): - if not isinstance(trackseries, LinaroSeries): - raise TypeError('The trackseries input must be of type LinaroSeries') - - self.nextseries=trackseries.toNextCandidate(strict=True) - - super(CandidateRN,self).__init__(proj, rnrepo, trackseries=trackseries, nextseries=self.nextseries) - -class ReleaseRN(RNSeries): - def __init__(self, proj, trackseries, rnrepo=None): - if not isinstance(trackseries, LinaroSeries): - raise TypeError('The series input must be of type LinaroSeries') - - nextseries=trackseries.toNextRelease(strict=True) - - super(ReleaseRN,self).__init__(proj, rnrepo, trackseries=trackseries, nextseries=nextseries) - -class TestCandidateRN(unittest.TestCase): - #logging.basicConfig(level="INFO") - testdirprefix="CandidateRNUT" - - @classmethod - def setUpClass(cls): - cls.proj=Proj(prefix=TestCandidateRN.testdirprefix, persist=True) - logging.info('11111 Created Proj dir') - cls.rnrepo=Clone(cls.proj,remote=u'http://git.linaro.org/toolchain/release-notes.git') - logging.info('22222 Created clone dir') - cls.branch=u'snapshots/linaro-6.1-2016.06' - cls.series=linaroSeriesFromBranchname(branch=cls.branch) - - @classmethod - def tearDownClass(cls): - cls.proj.cleanup() - - def test_candidate_newclone(self): - logging.info('33333 Going To Create CandidateRN') - candidatern=CandidateRN(self.proj, rnrepo=self.rnrepo.repodir, trackseries=self.series) - logging.info('44444 Created CandidateRN') - self.assertEqual(candidatern.nextseries.longuppertype(), "Release-Candidate") - self.assertEqual(candidatern.nextseries.longlowertype(), "release-candidate") - self.assertTrue(os.path.isdir(self.proj.projdir + "/release-notes.git")) - self.assertTrue(os.path.isdir(self.proj.projdir + "/candidate")) - self.assertTrue(os.path.isdir(self.proj.projdir + "/template")) - -# def test_candidate_existingclone(self): -# # Since this reuses an existing clone of a repository the -# # release-notes.git directory should actually be a symlink. -# candidatern=CandidateRN(self.proj, trackseries=self.series, rnrepo=TestCandidateRN.rnrepo.repodir) -# self.assertEqual(candidatern.series.longuppertype(), "Release-Candidate") -# self.assertEqual(candidatern.series.longlowertype(), "release-candidate") -# self.assertTrue(os.path.islink(self.proj.projdir + "/release-notes.git")) -# self.assertTrue(os.path.isdir(self.proj.projdir + "/candidate")) -# self.assertTrue(os.path.isdir(self.proj.projdir + "/template")) -# -# self.assertEqual(candidatern.rntemplate.getbranch().split("_")[0], "template") -## self.assertTrue(candidatern.rnworkdir.getbranch().startswith("releases/")) -# -# print "Based on: %s" % self.series.getBranchname() -# print "Template Branch: %s" % candidatern.rntemplate.getbranch() -# print "Candidate Branch: %s" % candidatern.rnworkdir.getbranch() + # self.rn_series.pushToBranch(self.next_series.getBranchname()) + +class TestSeriesRN(unittest.TestCase): + pass if __name__ == '__main__': + logging.basicConfig(level="INFO") unittest.main() diff --git a/linaropy/rn/template.py b/linaropy/rn/template.py index c036add..b5d0f59 100644 --- a/linaropy/rn/template.py +++ b/linaropy/rn/template.py @@ -10,7 +10,7 @@ from ..git.clone import Clone from ..git.workdir import Workdir from ..rninput import yninput -class TemplateRN(Workdir): +class RNTemplate(Workdir): _readme='components/toolchain/binaries/README.textile' _series='components/toolchain/binaries/README.textile.series' @@ -19,13 +19,17 @@ class TemplateRN(Workdir): branchname='template_' + branchname_suffix else: branchname='template_' + str(uuid.uuid4()) - super(TemplateRN, self).__init__(proj, rnclone, "template", track="origin/master", branchname=branchname) + super(RNTemplate, self).__init__(proj, rnclone, "template", track="origin/master", branchname=branchname) # Todo move template workdir out of detached head state. # TODO: Allow the user to edit BOTH the gcc-linaro/ and binaries/ # README.textile file as currently this function only does the binary one. # TODO: Create Unit Test - def updateTemplateReadme(self): + # TODO: test headless + def update_readme(self, headless=False): + if headless: + return False + answer=yninput("Would you like to update the Template README.textile file?", "N") if answer != "y": print "The Template README.textile file has not been updated." @@ -35,13 +39,17 @@ class TemplateRN(Workdir): if not editor: editor='/usr/bin/vim' print "Edit the README.textile template fragment if changes are needed." - os.system(editor + ' ' + self.repodir + '/' + TemplateRN._readme) + os.system(editor + ' ' + self.repodir + '/' + RNTemplate._readme) - self.add(TemplateRN._readme) + self.add(RNTemplate._readme) return True # TODO: Create Unit Test - def updateTemplateSeries(self): + # TODO: test headless + def update_series(self, headless=False): + if headless: + return False + answer=yninput("Would you like to update the Template README.textile.series file?", "N") if answer != "y": print "The Template README.textile.series file has not been updated." @@ -51,40 +59,42 @@ class TemplateRN(Workdir): if not editor: editor='/usr/bin/vim' print "Edit the README.textile.series template fragment if changes are needed." - os.system(editor + ' ' + self.repodir + '/' + TemplateRN._series) + os.system(editor + ' ' + self.repodir + '/' + RNTemplate._series) - self.add(TemplateRN._series) + self.add(RNTemplate._series) return True - def getReadme(self): - return self.repodir + '/' + TemplateRN._readme + # TODO: Do we need this? + def get_readme(self): + return self.repodir + '/' + RNTemplate._readme - def getSeries(self): - return self.repodir + '/' + TemplateRN._series + # TODO: Do we need this? + def get_series(self): + return self.repodir + '/' + RNTemplate._series -class TestTemplateRN(unittest.TestCase): - testdirprefix="TemplateRNUT" +class TestRNTemplate(unittest.TestCase): + testdirprefix="RNTemplateUT" # Every instance of TestClass requires its own proj directory. def setUp(self): - self.proj=Proj(prefix=TestTemplateRN.testdirprefix, persist=True) + self.proj=Proj(prefix=TestRNTemplate.testdirprefix, persist=True) def tearDown(self): self.proj.cleanup() def test_getREADME(self): self.rnrepo=Clone(self.proj,remote=u'http://git.linaro.org/toolchain/release-notes.git') - self.rntemplate=TemplateRN(self.proj,self.rnrepo) - compare=self.proj.projdir + '/template/' + TemplateRN._readme - self.assertEqual(self.rntemplate.getReadme(),compare) - self.assertTrue(os.path.isfile(self.rntemplate.getReadme())) + self.rn_template=RNTemplate(self.proj,self.rnrepo) + compare=self.proj.projdir + '/template/' + RNTemplate._readme + self.assertEqual(self.rn_template.get_readme(),compare) + self.assertTrue(os.path.isfile(self.rn_template.get_readme())) def test_getSERIES(self): self.rnrepo=Clone(self.proj,remote=u'http://git.linaro.org/toolchain/release-notes.git') - self.rntemplate=TemplateRN(self.proj,self.rnrepo) - compare=self.proj.projdir + '/template/' + TemplateRN._series - self.assertEqual(self.rntemplate.getSeries(),compare) - self.assertTrue(os.path.isfile(self.rntemplate.getSeries())) + self.rn_template=RNTemplate(self.proj,self.rnrepo) + compare=self.proj.projdir + '/template/' + RNTemplate._series + self.assertEqual(self.rn_template.get_series(),compare) + self.assertTrue(os.path.isfile(self.rn_template.get_series())) # TODO: Test branchname_suffix diff --git a/linaropy/series.py b/linaropy/series.py index 3e6830c..cad1e31 100644 --- a/linaropy/series.py +++ b/linaropy/series.py @@ -2,6 +2,7 @@ import unittest import logging import os import uuid +import string from vers import Spin from vers import Rc @@ -146,8 +147,9 @@ class Series(object): '%R': self._getRc, } + def _getPackageName(self): - return self.package.pv + return self.package.package def _getPackageNameLower(self): return self.package.lower() @@ -174,7 +176,6 @@ class Series(object): ret='' # Iterate across the dictionary and for each key found and invoke the # function. - import string ret=format_spec for key in self.fmt.iterkeys(): replac=self.fmt[key]() @@ -239,7 +240,7 @@ class Series(object): return branchname -def seriesFromBranchname(branch=None): +def series_from_branchname(branch=None): """ Create a Series from a branch name string. @@ -264,30 +265,36 @@ def seriesFromBranchname(branch=None): ------- releases/<foo>[!-rcN] """ + logging.info("Branch is %s." % branch) if not isinstance(branch, basestring): - raise TypeError('seriesFromBranchname requires a basestring as input') + raise TypeError('series_from_branchname requires a basestring as input') if not branch: - raise ValueError('seriesFromBranchname requires a non-empty string as input') + raise TypeError('series_from_branchname requires a non-empty string as input') # Get the part before the '/'. That's the namespace try: namespace=branch.rsplit('/', 1)[0] except IndexError: - raise RuntimeError('string must be <namespace>/<everything_else>') + raise ValueError('string must have a namespace, e.g., <namespace>/<everything_else>') # Get everything after the '/'. try: seriesstr=branch.rsplit('/', 1)[1] except IndexError: - raise RuntimeError('string must be <package_name>-<package_version>') + raise ValueError("string must delimit the namespace from the right branch with a '/' char, e.g., <namespace>/<package_name>-<package_version>") - if namespace == u'': - raise TypeError("Couldn't parse a namespace from input string") + # TODO: Are these necessary or did the previous cases cover it? + #if namespace == u'': + # raise ValueError("Couldn't parse a namespace from input string") + # This will catch where there's a namespace/ but no right hand value after + # the /. if seriesstr == u'': - raise TypeError("Couldn't parse a series from input string") + raise ValueError("Couldn't parse a series from input string. Missing a branch name.") keys=['vendor', 'version', 'date', 'spin', 'rc'] + # This might generate key errors, depending on whether anything can be + # parsed from the right-hand values. values=seriesstr.split('-') dictionary=dict(zip(keys, values)) @@ -306,11 +313,16 @@ def seriesFromBranchname(branch=None): # strip the "rc" and just leave the int. if dictionary["rc"].startswith("rc"): dictionary["rc"]=dictionary["rc"][2:] + if not dictionary["rc"].isnumeric(): + raise TypeError("The rc value must be numeric.") # We need to have None fields in the missing keys for when we call the # Series constructor. if not dictionary.has_key("spin"): dictionary["spin"]=None + else: + if not dictionary["spin"].isnumeric(): + raise TypeError("The spin must be numeric.") seriesdate=datetime.today if dictionary["date"]: @@ -322,9 +334,9 @@ def seriesFromBranchname(branch=None): seriesdate=datetime(int(datefields["year"]), int(datefields["month"]), int(datefields["day"])) if "snapshots" in namespace and dictionary["rc"]: - raise RuntimeError('A snapshots namespace can not have an rc. This is a non-conforming input.') + raise ValueError('A snapshots namespace can not have an rc. This is a non-conforming input.') elif "releases" not in namespace and dictionary["rc"]: - raise RuntimeError('An rc must have a "releases" namespace. This is a non-conforming input.') + raise ValueError('An rc must have a "releases" namespace. This is a non-conforming input.') elif dictionary["rc"]: seriesname="candidate" elif "snapshots" in namespace: @@ -332,16 +344,65 @@ def seriesFromBranchname(branch=None): elif "releases" in namespace: seriesname="release" elif "snapshot" in namespace: - raise RuntimeError('"snapshot" is not a complete namespace. A conforming namespace is "snapshots".') + raise ValueError('"snapshot" is not a complete namespace. The conforming namespace is "snapshots".') else: # TODO test for unknown namespace. - raise RuntimeError('Unknown namespace in input string.') + raise ValueError('Unknown namespace in input string.') package=Package(package="GCC", version=dictionary["version"]) series=Series(seriesname, package=package, date=seriesdate, spin=dictionary["spin"], rc=dictionary["rc"], strict=True) return series +def series_from_tag(tag=None): + """ + Create a Series from a git tag name + + Parameters + ---------- + tag : str + + The git tag of a package as it lives in a git repository. A properly + formed tag will take the following form: + + Snapshot + -------- + linaro-snapshot-Maj.min-YYYY.MM[-spin] + + Candidate + --------- + linaro-Maj.min-YYYY.MM[-spin]-rcN + + Release + ------- + linaro-Maj.min-YYYY.MM[-spin] + """ + + # TODO Test this. + if not tag: + raise ValueError('series_from_tag requires a tag') + + # TODO Test this. + if not isinstance(tag, basestring): + raise TypeError('series_from_tag requires a basestring as input') + + # This is default, we'll replace with 'snapshots' if we detect that the + # tag is indeed a snapshot. + namespace="releases" + + # since 'snapshots' tags aren't the same as release and rc tags we need + # to force conformance, i.e., remove "-snapshot", but record the + # namespace as snapshots while we're at it. + if "snapshot" in tag: + tag=string.replace(tag,"-snapshot",'') + namespace="snapshots" + + # Now we're going to cheat and fabricate a false branchname. + branchname=namespace + '/' + tag + + # ... in which case we can reuse this function. + return series_from_branchname(branchname) + from vers import versionFromStr class TestSeries(unittest.TestCase): @@ -544,55 +605,131 @@ class TestSeries(unittest.TestCase): release=Series("release", package="GCC-5.3.1", date=datetime(2016,05,15), rc="1", strict=False) self.assertEqual(release.getBranchname(), "releases/linaro-5.3-2016.05-rc1") - # TODO: Test combinations of branch names with and without spins/rcs. - - # TODO: Test whether seriesFromBranchname can detect non-conforming branch names. - # These tests will verify that a branchname can be read, a series created, # and then the same branch name recreated. - def test_seriesFromBranchname(self): - branch=u'snapshots/linaro-5.3-2016.05-6-rc1' - with self.assertRaises(RuntimeError): - series=seriesFromBranchname(branch=branch) + def test_series_from_branchname(self): branch2=u'snapshots/linaro-5.3-2016.05-6' - series2=seriesFromBranchname(branch=branch2) + series2=series_from_branchname(branch=branch2) self.assertEqual(series2.getBranchname(),u'snapshots/linaro-5.3-2016.05-6') branch3=u'snapshots/linaro-5.3-2016.05' - series3=seriesFromBranchname(branch=branch3) + series3=series_from_branchname(branch=branch3) self.assertEqual(series3.getBranchname(),u'snapshots/linaro-5.3-2016.05') branch4=u'releases/linaro-5.3-2016.05-6-rc1' - series4=seriesFromBranchname(branch=branch4) + series4=series_from_branchname(branch=branch4) self.assertEqual(series4.getBranchname(),u'releases/linaro-5.3-2016.05-6-rc1') branch5=u'releases/linaro-5.3-2016.05-rc1' - series5=seriesFromBranchname(branch=branch5) + series5=series_from_branchname(branch=branch5) self.assertEqual(series5.getBranchname(),u'releases/linaro-5.3-2016.05-rc1') branch6=u'releases/linaro-5.3-2016.05-6' - series6=seriesFromBranchname(branch=branch6) + series6=series_from_branchname(branch=branch6) self.assertEqual(series6.getBranchname(),u'releases/linaro-5.3-2016.05-6') branch7=u'releases/linaro-5.3-2016.05' - series7=seriesFromBranchname(branch=branch7) + series7=series_from_branchname(branch=branch7) self.assertEqual(series7.getBranchname(),u'releases/linaro-5.3-2016.05') + # -rc1 is invalid with a snapshots namespace. branch8=u'snapshots/linaro-5.3-2016.05-6-rc1' - with self.assertRaises(RuntimeError): - series8=seriesFromBranchname(branch=branch8) + with self.assertRaises(ValueError): + series8=series_from_branchname(branch=branch8) + # Wrong branchname.. it should be 'snapshots' branch9=u'snapshot/linaro-5.3-2016.05-6-rc1' - with self.assertRaises(RuntimeError): - series9=seriesFromBranchname(branch=branch9) + with self.assertRaises(ValueError): + series9=series_from_branchname(branch=branch9) + # Wrong branchname.. it should be 'snapshots' branch10=u'snapshot/linaro-5.3-2016.05-6' - with self.assertRaises(RuntimeError): - series10=seriesFromBranchname(branch=branch10) + with self.assertRaises(ValueError): + series10=series_from_branchname(branch=branch10) + + # namespace required. + branch11=u'linaro-5.3-2016.05-6' + with self.assertRaises(ValueError): + series11=series_from_branchname(branch=branch11) + + # it should complain about missing the right-hand values. + branch12=u'snapshots/' + with self.assertRaises(ValueError): + series12=series_from_branchname(branch=branch12) + + # It won't parse because of a missing namespace / + branch13=u'snapshots' + with self.assertRaises(ValueError): + series13=series_from_branchname(branch=branch13) + + branch14=u'snapshots/foo' + with self.assertRaises(KeyError): + series14=series_from_branchname(branch=branch14) + + # unknown namespace. + branch15=u'foobarnamespace/linaro-5.3-2016.05-6' + with self.assertRaises(ValueError): + series15=series_from_branchname(branch=branch15) + + # This will fail on a non-datetime input. + branch16=u'snapshots/linaro-5.3-asdf' + with self.assertRaises(ValueError): + series16=series_from_branchname(branch=branch16) + + # This will fail with an invalid spin. + branch17=u'snapshots/linaro-5.3-2016.05-a' + with self.assertRaises(TypeError): + series17=series_from_branchname(branch=branch17) + + # This will fail with an invalid rc. + branch18=u'snapshots/linaro-5.3-2016.05-rcasdfn' + with self.assertRaises(TypeError): + series18=series_from_branchname(branch=branch18) + + # This will fail with an invalid rc. + branch19=u'snapshots/linaro-5.3-2016.05-9-rcasdfn' + with self.assertRaises(TypeError): + series19=series_from_branchname(branch=branch19) # TODO: Test series.label (as there was a runtime bug) + def test_snapshot_series_from_tag(self): + tag=u'linaro-snapshot-5.3-2016.05-6' + series=series_from_tag(tag=tag) + self.assertEqual(series.getBranchname(),u'snapshots/linaro-5.3-2016.05-6') + + def test_invalid_snapshot_series_from_tag(self): + # We can't have -rc1 on a snapshot. + tag=u'linaro-snapshot-5.3-2016.05-6-rc1' + with self.assertRaises(ValueError): + series=series_from_tag(tag=tag) + + def test_candidate_series_from_tag(self): + tag=u'linaro-5.3-2016.05-6-rc1' + series=series_from_tag(tag=tag) + self.assertEqual(series.getBranchname(),u'releases/linaro-5.3-2016.05-6-rc1') + + def test_release_series_from_tag(self): + tag=u'linaro-5.3-2016.05-6' + series=series_from_tag(tag=tag) + self.assertEqual(series.getBranchname(),u'releases/linaro-5.3-2016.05-6') + + def test_series_from_tag_invalid_spin(self): + tag=u'linaro-5.3-2016.05-abc' + with self.assertRaises(TypeError): + series=series_from_tag(tag=tag) + + def test_series_from_tag_invalid_rc(self): + tag=u'linaro-5.3-2016.05-rcabf' + with self.assertRaises(TypeError): + series=series_from_tag(tag=tag) + + def test_series_from_tag_invalid_rc_with_valid_spin(self): + tag=u'linaro-5.3-2016.05-9-rcabf' + with self.assertRaises(TypeError): + series=series_from_tag(tag=tag) + if __name__ == '__main__': #logging.basicConfig(level="INFO") unittest.main() @@ -16,11 +16,13 @@ from linaropy.rn.gccclone import GCCClone from linaropy.git.clone import Clone from linaropy.rn.linaroseries import LinaroSeries from linaropy.rn.linaroseries import linaroSeriesFromBranchname +from linaropy.rn.linaroseries import linaro_series_from_tag from linaropy.proj import Proj from linaropy.git.gitrepo import cd -from linaropy.rn.template import TemplateRN -from linaropy.rn.rnseries import CandidateRN +from linaropy.rn.template import RNTemplate +from linaropy.rn.rnseries import RNSeries from linaropy.rn.rngen import rngen +from linaropy.rninput import yninput rnProj=[] @@ -30,16 +32,16 @@ def rncleanup(): # This will be the case if the script is run via the test driver. print "No cleanup needed" else: + print "Cleaning up Proj dir %s if possible." % rnProj[0].projdir rnProj[0].cleanup() -def generate(track, todate, toseries, gccsource, persist): +def generate(track, to_date, to_series, gccsource, persist): # Delay creating the Proj directory until now so that the parser (and # parser validation functions) can be tested in the unittests without # invoking the internals of this driver. rnProj.append(Proj(prefix='rn', persist=persist)) print "proj dir is: %s with persist=%s" % (rnProj[0].projdir, str(persist)) - # This will raise an exception if gccsource is not a git repository. gccclone=GCCClone(rnProj[0], clonedir=gccsource) @@ -47,36 +49,54 @@ def generate(track, todate, toseries, gccsource, persist): print 'gccbaseversion is ' + gccclone.get_base_version() print 'fsf revision is ' + gccclone.get_fsf_revision() - trackSeries=linaroSeriesFromBranchname(track) - - #rnclone=Clone(rnProj[0], remote=u'http://git.linaro.org/toolchain/release-notes.git') - rnclone=Clone(rnProj[0], remote=u'ssh://git@git.linaro.org/toolchain/release-notes.git') - - # TODO: Check toseries to create the right *RN object. - nexRN=CandidateRN(rnProj[0], rnrepo=rnclone.clonedir(), trackseries=trackSeries) + if gccclone.tag_exists(track): + logging.info("%s is a tag. Creating Series from tag.") + track_series=linaro_series_from_tag(track) + else: + logging.info("%s is a branch? Creating Series from branchname.") + track_series=linaroSeriesFromBranchname(track) + + try: + next_series=track_series.toNext(to_series) + except TypeError: + print( + "Next series '%s' from '%s' in an invalid progression" + % (LinaroSeries.series[to_series], track_series.shorttype())) + print("If this is a release series try tracking the release-candidate tag instead of the release branch.") + sys.exit(2) + + if to_date != next_series.date: + raise RuntimeError('The date passed to this driver does not equal the date computed by LinaroSeries.toNext()') - nextSeries=nexRN.get_next_series() + rnclone=Clone( + rnProj[0], + remote=u'ssh://git@git.linaro.org/toolchain/release-notes.git') - if todate != nextSeries.date: - raise RuntimeError('The date passed to this driver does not equal the date computed by LinaroSeries.toNext()') -# print "toNext: " + nextSeries.getBranchname() -# print "toNext: " + format(nextSeries, '%N/%d') + next_rn=RNSeries( + rnProj[0], + rnrepo=rnclone.clonedir(), + track_series=track_series, + next_series=next_series) - # TODO: What if 'track' is not a snapshot branch? To do this right we'd - # probably have to preserve basedon information in the release notes - # repository as a yaml file. -# basedon='http://snapshots.linaro.org/components/toolchain/gcc-linaro/' -# basedon=basedon + trackSeries.getDir() -# print "basedon: " + basedon + if next_rn.update_templ_readmes(): + print "Please verify that your changes have been committed on the template branch:" + self.rn_template.log(1) - rngen(nexRN, gccclone) + ans="y" + while not ans=="n": + if next_rn.update_series_readmes(): + print "Please verify that your changes have been committed on the series branch:" + next_rn.rn_series.log(1) -# print 'gccbaseversion is ' + gccclone.get_base_version() -# print 'fsf revision is ' + gccclone.get_fsf_revision() + # Generate the temporary output files to the projdir. + with cd(rnProj[0].projdir): + rngen(next_rn, gccclone) - # TODO: After the release notes are generated, the generated files need - # to be added, commit, and pushed. - # These changes are on the CandidateRN workdir branch. + print "Please direct your browser to the rendered .html files in" + print "%s and make sure that they look correct." % rnProj[0].projdir + print "Would you like to edit the series README.textile.series file" + ans=yninput( + "again and regenerate the release notes?", "N") # Verify that the GCC Source is located where it says it is. class VerifyGCCSourceAction(argparse.Action): @@ -111,28 +131,28 @@ def create_parser(): ) # Positionals are required by default. - parser.add_argument('track', help='branchname of series to track.') + parser.add_argument('track', help='branchname or tag name of series to track.') parser.add_argument('-g', '--gccsource', dest='gccsource', required=True, action=VerifyGCCSourceAction, help='location of the gcc source') - parser.add_argument('-d', '--date', dest='todate', required=True, help='the next series date in "YYYY.MM" form.', type=str_to_datetime) + parser.add_argument('-d', '--date', dest='to_date', required=True, help='the next series date in "YYYY.MM" form.', type=str_to_datetime) parser.add_argument('-n', '--nopersist', dest='persist', default=True, action='store_false', help='The proj dir will not persist once this program has executed.') # At least one of the following arguments are required. group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('-c', '--candidate', dest='toseries', action='store_const', const=LinaroSeries.series.index("candidate")) - group.add_argument('-r', '--release', dest='toseries', action='store_const', const=LinaroSeries.series.index("release")) - group.add_argument('-s', '--snapshot', dest='toseries', action='store_const', const=LinaroSeries.series.index("snapshot")) + group.add_argument('-c', '--candidate', dest='to_series', action='store_const', const=LinaroSeries.series.index("candidate")) + group.add_argument('-r', '--release', dest='to_series', action='store_const', const=LinaroSeries.series.index("release")) + group.add_argument('-s', '--snapshot', dest='to_series', action='store_const', const=LinaroSeries.series.index("snapshot")) return parser def main(): parser=create_parser() args = parser.parse_args() - generate(args.track, args.todate, args.toseries, args.gccsource, args.persist) + generate(args.track, args.to_date, args.to_series, args.gccsource, args.persist) if __name__ == '__main__': -# logging.basicConfig(level="INFO") + logging.basicConfig(level="INFO") with handle_exit(rncleanup): main() diff --git a/tcwg-release.sh b/tcwg-release.sh index 73dfb97..495ef0b 100755 --- a/tcwg-release.sh +++ b/tcwg-release.sh @@ -937,8 +937,10 @@ mydir="$(dirname "$(readlink -f "$0")")" print_info "${bold}Generating release notes into ${REL_DIR}/git/" +next_series="${SNAPSHOT:+-s}${RELEASE:+-r}${CANDIDATE:+-c}" + set -x - python ${mydir}/rn.py ${track} -c -g ${REL_DIR}/git --date ${DATE} + python ${mydir}/rn.py ${track} ${next_series} -g ${REL_DIR}/git --date ${DATE} set +x popd 1>/dev/null |