def show(ctx, recursive, with_tags, directory): """ current release version. If a single directory is specified, it will print out the current release version in the form of `<release>[<-sha-commit>[-dirty]]`. If multiple directories are specified, it will print out the directory name followed by the release version. """ release_infos = ReleaseInfo.find_all(directory, recursive, ctx.obj["dry_run"]) for release_info in release_infos: if not release_info.has_release_configuration: log.error( f"directory {release_info.directory} has no release configuration" ) exit(1) if recursive: if with_tags: print( f'{release_info.directory}\t{release_info.current_version}\t{release_info.tag}' ) else: print( f'{release_info.directory}\t{release_info.current_version}' ) else: print(release_info.current_version)
def __init__(self, path: str, dry_run: bool = False): super(ReleaseInfo, self).__init__() self.dry_run = dry_run self.directory = path self.path = os.path.join(self.directory, ".release") self.base_tag = None self._semver = None self._pre_tag_command = None if not os.path.isdir(self.directory): log.error(f"directory {self.directory} does not exist") exit(1) if self.has_release_configuration: self.read()
def next_version(self, level): assert self.semver release = list(map(lambda n: int(n), self.semver.split("."))) if level == ReleaseInfo.PATCH: release[ReleaseInfo.PATCH] += 1 elif level == ReleaseInfo.MINOR: release[ReleaseInfo.MINOR] += 1 release[ReleaseInfo.PATCH] = 0 elif level == ReleaseInfo.MAJOR: release[ReleaseInfo.MAJOR] += 1 release[ReleaseInfo.MINOR] = 0 release[ReleaseInfo.PATCH] = 0 else: log.error("I can only bump PATCH, MINOR or MAJOR levels") exit(1) self.semver = "%d.%d.%d" % (release[0], release[1], release[2])
def tag_next_release(self, level, message: str = None, force: bool = False): if not force: if not self.changes_since_tag: log.info( f"{self.directory} has no changes since {self.semver}.") return self.next_version(level) if self.tag in self.all_tags: log.error(f"tag {self.tag} already exists") exit(1) self.write() if not message: message = f"bumped {self.git_prefix} to release {self.semver}" self.commit_and_tag(message)
def exec_pre_tag_command(self): if self.dry_run and self.pre_tag_command: log.debug(f"$ {self.pre_tag_command}") return if self.pre_tag_command: cmd = self.process_pre_tag_command() process = subprocess.Popen( cmd, shell=True, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out = process.communicate() if process.returncode != 0: log.error( f"{self.pre_tag_command} in {self.directory}, returned {process.returncode}, output {out[1]}" ) exit(1) return out[0]
def read(self): result = {} with open(self.path, "r") as f: for line in f: line = line.rstrip() if len(line) > 0 and line[0] != "#": value = line.split("=", 1) result[value[0].strip()] = value[1].strip() if not ("release" in result and "tag" in result): log.error( f"{self.path} does not contain release and/or tag values") exit(1) if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", result["release"]): log.error( f"ERROR: incorrect format of release in {self.path}, expected <major.minor.patch>" ) exit(1) match = re.fullmatch( r"(?P<base_tag>.*)(?P<release>[0-9]+\.[0-9]+\.[0-9]+$)", result["tag"]) if not match: log.error( f"ERROR: incorrect format old the tag in {self.path}, expected tag <base><major.minor.patch>" ) exit(1) self.semver = result["release"] self.pre_tag_command = result.get("pre_tag_command") self.base_tag = match.group("base_tag") if match.group("release") != self.semver: log.warning( f"tag {self.tag} in {self.path} does not match specified release {match.group('release')}" )
def validate(release_infos: List["ReleaseInfo"]) -> bool: result = True base_tags = {} for release_info in release_infos: base_tag = release_info.base_tag existing = base_tags.get(base_tag) if release_info.base_tag in base_tags: log.error( f"{release_info.path} has the same base tag as {existing.path}: {base_tag}" ) result = False else: base_tags[base_tag] = release_info if not release_info.is_inside_work_tree: log.error( f"{release_info.directory} is not inside a git workspace") result = False else: if release_info.tag not in release_info.all_tags: log.error( f"tag {release_info.tag} in {release_info.path} does not exist in repository" ) result = False return result
def exec(cmd: List[str], cwd: str, dry_run:bool=False, fail_on_error: bool = True): log.debug("$ %s #cwd = %s", _to_cli(cmd), cwd) if dry_run: return ("", ""), None process = subprocess.Popen( cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) out = process.communicate() log.debug("returncode = %s", process.returncode) log.debug("stdout = %s", out[0]) log.debug("stderr = %s", out[1]) if fail_on_error and process.returncode != 0: log.error("%s failed in %s, %s", " ".join(cmd), cwd, (out[0]+out[1])) exit(1) return out, process
def initialize(ctx, initial_release, tag_prefix, pre_tag_command, directory): """ directory with release configuration. Creates a release configuration using the specified `initial-release`, `tag-prefix`, and `pre-tag-command`. the file is called .release and has the following syntax: \b release=<initial-release> tag=<tag-prefix><initial-release> pre-tag-command=<pre-tag-command> The `pre-tag-command` is executed and any outstanding changes are committed and tagged with the specified `tag`. The directories must be in a git workspace. """ print(f'>{pre_tag_command}<') directories = sorted(directory, key=lambda p: len(os.path.abspath(p).split("/")), reverse=True) prefixes = list( map(lambda d: os.path.basename(os.path.abspath(d)), directories)) if tag_prefix is not None and len(directories) > 1: log.error( 'you cannot specify the same tag-prefix for different directories') exit(1) if len(set(prefixes)) != len(prefixes): log.error( 'base directory names must be unique in order to avoid non unique tag-prefixes' ) exit(1) for path in directories: component = ReleaseInfo(path) if not component.is_inside_work_tree: log.error('%s is not inside a git workspace, please run git init', path) exit(1) result = True for path in directories: result = ReleaseInfo.initialize( path, semver=initial_release, base_tag=tag_prefix if tag_prefix is not None else f'{os.path.basename(os.path.abspath(path))}-', pre_tag_command=pre_tag_command, dry_run=ctx.obj["dry_run"], ) and result exit(not result)
def initialize( directory: str, semver: str = "0.0.0", base_tag: str = "", pre_tag_command: str = "", dry_run: bool = False, ) -> bool: if not os.path.isdir(directory): log.error(f"{directory} is not a directory.") exit(1) path = os.path.join(directory, ".release") if os.path.exists(path): log.error(f"{directory} is ready initialized.") return False info = ReleaseInfo(path=directory, dry_run=dry_run) info.semver = semver info.base_tag = base_tag info.pre_tag_command = pre_tag_command if info.is_inside_work_tree and info.tag in info.all_tags: log.error( f'tag {info.tag} already exist in git repository for {info.path}' ) exit(1) info.write() if info.is_inside_work_tree: info.commit_and_tag( f"initialized {info.git_prefix} to release {info.semver}") else: log.warning(f"{info.path} is not inside a git workspace") return True