private void UpdateVersion(string projectDirectory, Repository repository, SemanticVersion oldVersion, SemanticVersion newVersion) { Requires.NotNull(projectDirectory, nameof(projectDirectory)); Requires.NotNull(repository, nameof(repository)); var signature = this.GetSignature(repository); var versionOptions = VersionFile.GetVersion(repository, projectDirectory); if (IsVersionDecrement(oldVersion, newVersion)) { this.stderr.WriteLine($"Cannot change version from {oldVersion} to {newVersion} because {newVersion} is older than {oldVersion}."); throw new ReleasePreparationException(ReleasePreparationError.VersionDecrement); } if (!EqualityComparer <SemanticVersion> .Default.Equals(versionOptions.Version, newVersion)) { if (versionOptions.VersionHeightPosition.HasValue && GitExtensions.WillVersionChangeResetVersionHeight(versionOptions.Version, newVersion, versionOptions.VersionHeightPosition.Value)) { // The version will be reset by this change, so remove the version height offset property. versionOptions.VersionHeightOffset = null; } versionOptions.Version = newVersion; var filePath = VersionFile.SetVersion(projectDirectory, versionOptions, includeSchemaProperty: true); Commands.Stage(repository, filePath); // Author a commit only if we effectively changed something. if (!repository.Head.Tip.Tree.Equals(repository.Index.WriteToTree())) { repository.Commit($"Set version to '{versionOptions.Version}'", signature, signature, new CommitOptions() { AllowEmptyCommit = false }); } } }
/// <summary> /// Tests whether a commit is of a specified version, comparing major and minor components /// with the version.txt file defined by that commit. /// </summary> /// <param name="commit">The commit to test.</param> /// <param name="expectedVersion">The version to test for in the commit</param> /// <param name="comparisonPrecision">The last component of the version to include in the comparison.</param> /// <param name="repoRelativeProjectDirectory">The repo-relative directory from which <paramref name="expectedVersion"/> was originally calculated.</param> /// <returns><c>true</c> if the <paramref name="commit"/> matches the major and minor components of <paramref name="expectedVersion"/>.</returns> internal static bool CommitMatchesVersion(this Commit commit, SemanticVersion expectedVersion, SemanticVersion.Position comparisonPrecision, string repoRelativeProjectDirectory) { Requires.NotNull(commit, nameof(commit)); Requires.NotNull(expectedVersion, nameof(expectedVersion)); var commitVersionData = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); var semVerFromFile = commitVersionData?.Version; if (semVerFromFile == null) { return(false); } // If the version height position moved, that's an automatic reset in version height. if (commitVersionData.VersionHeightPosition != comparisonPrecision) { return(false); } if (comparisonPrecision == SemanticVersion.Position.Prerelease) { // The entire version spec must match exactly. return(semVerFromFile?.Equals(expectedVersion) ?? false); } for (SemanticVersion.Position position = SemanticVersion.Position.Major; position <= comparisonPrecision; position++) { int expectedValue = ReadVersionPosition(expectedVersion.Version, position); int actualValue = ReadVersionPosition(semVerFromFile.Version, position); if (expectedValue != actualValue) { return(false); } } return(true); }
/// <summary> /// Encodes a commit from history in a <see cref="Version"/> /// so that the original commit can be found later. /// </summary> /// <param name="commit">The commit whose ID and position in history is to be encoded.</param> /// <param name="repoRelativeProjectDirectory">The repo-relative project directory for which to calculate the version.</param> /// <param name="versionHeight"> /// The version height, previously calculated by a call to <see cref="GetVersionHeight(Commit, string)"/> /// with the same value for <paramref name="repoRelativeProjectDirectory"/>. /// </param> /// <returns> /// A version whose <see cref="Version.Build"/> and /// <see cref="Version.Revision"/> components are calculated based on the commit. /// </returns> /// <remarks> /// In the returned version, the <see cref="Version.Build"/> component is /// the height of the git commit while the <see cref="Version.Revision"/> /// component is the first four bytes of the git commit id (forced to be a positive integer). /// </remarks> public static Version GetIdAsVersion(this Commit commit, string repoRelativeProjectDirectory = null, int?versionHeight = null) { var versionOptions = VersionFile.GetVersion(commit, repoRelativeProjectDirectory); return(GetIdAsVersionHelper(commit, versionOptions, repoRelativeProjectDirectory, versionHeight)); }
/// <summary> /// Initializes a new instance of the <see cref="VersionOracle"/> class. /// </summary> public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, LibGit2Sharp.Commit head, ICloudBuild cloudBuild, int?overrideBuildNumberOffset = null, string projectPathRelativeToGitRepoRoot = null) { var repoRoot = repo?.Info?.WorkingDirectory?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var relativeRepoProjectDirectory = !string.IsNullOrWhiteSpace(repoRoot) ? (!string.IsNullOrEmpty(projectPathRelativeToGitRepoRoot) ? projectPathRelativeToGitRepoRoot : projectDirectory.Substring(repoRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) : null; var commit = head ?? repo?.Head.Commits.FirstOrDefault(); var committedVersion = VersionFile.GetVersion(commit, relativeRepoProjectDirectory); var workingVersion = head != null?VersionFile.GetVersion(head, relativeRepoProjectDirectory) : VersionFile.GetVersion(projectDirectory); if (overrideBuildNumberOffset.HasValue) { if (committedVersion != null) { committedVersion.BuildNumberOffset = overrideBuildNumberOffset.Value; } if (workingVersion != null) { workingVersion.BuildNumberOffset = overrideBuildNumberOffset.Value; } } this.VersionOptions = committedVersion ?? workingVersion; this.GitCommitId = commit?.Id.Sha ?? cloudBuild?.GitCommitId ?? null; this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion); this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? repo?.Head.CanonicalName; // Override the typedVersion with the special build number and revision components, when available. if (repo != null) { this.Version = GetIdAsVersion(commit, committedVersion, workingVersion, this.VersionHeight); } else { this.Version = this.VersionOptions?.Version.Version ?? Version0; } this.VersionHeightOffset = this.VersionOptions?.BuildNumberOffsetOrDefault ?? 0; this.PrereleaseVersion = this.ReplaceMacros(this.VersionOptions?.Version?.Prerelease ?? string.Empty); this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumberOrDefault ?? VersionOptions.CloudBuildNumberOptions.DefaultInstance; if (!string.IsNullOrEmpty(this.BuildingRef) && this.VersionOptions?.PublicReleaseRefSpec?.Length > 0) { this.PublicRelease = this.VersionOptions.PublicReleaseRefSpec.Any( expr => Regex.IsMatch(this.BuildingRef, expr)); } }
/// <summary> /// Prepares a release for the specified directory by creating a release branch and incrementing the version in the current branch. /// </summary> /// <exception cref="ReleasePreparationException">Thrown when the release could not be created.</exception> /// <param name="projectDirectory"> /// The path to the directory which may (or its ancestors may) define the version file. /// </param> /// <param name="releaseUnstableTag"> /// The prerelease tag to add to the version on the release branch. Pass <c>null</c> to omit/remove the prerelease tag. /// The leading hyphen may be specified or omitted. /// </param> /// <param name="nextVersion"> /// The next version to save to the version file on the current branch. Pass <c>null</c> to automatically determine the next /// version based on the current version and the <c>versionIncrement</c> setting in <c>version.json</c>. /// Parameter will be ignored if the current branch is a release branch. /// </param> /// <param name="versionIncrement"> /// The increment to apply in order to determine the next version on the current branch. /// If specified, value will be used instead of the increment specified in <c>version.json</c>. /// Parameter will be ignored if the current branch is a release branch. /// </param> public void PrepareRelease(string projectDirectory, string releaseUnstableTag = null, Version nextVersion = null, VersionOptions.ReleaseVersionIncrement?versionIncrement = null) { Requires.NotNull(projectDirectory, nameof(projectDirectory)); // open the git repository var repository = this.GetRepository(projectDirectory); if (repository.Info.IsHeadDetached) { this.stderr.WriteLine("Detached head. Check out a branch first."); throw new ReleasePreparationException(ReleasePreparationError.DetachedHead); } // get the current version var versionOptions = VersionFile.GetVersion(projectDirectory); if (versionOptions == null) { this.stderr.WriteLine($"Failed to load version file for directory '{projectDirectory}'."); throw new ReleasePreparationException(ReleasePreparationError.NoVersionFile); } var releaseBranchName = this.GetReleaseBranchName(versionOptions); var originalBranchName = repository.Head.FriendlyName; var releaseVersion = string.IsNullOrEmpty(releaseUnstableTag) ? versionOptions.Version.WithoutPrepreleaseTags() : versionOptions.Version.SetFirstPrereleaseTag(releaseUnstableTag); // check if the current branch is the release branch if (string.Equals(originalBranchName, releaseBranchName, StringComparison.OrdinalIgnoreCase)) { this.stdout.WriteLine($"{releaseBranchName} branch advanced from {versionOptions.Version} to {releaseVersion}."); this.UpdateVersion(projectDirectory, repository, versionOptions.Version, releaseVersion); return; } var nextDevVersion = this.GetNextDevVersion(versionOptions, nextVersion, versionIncrement); // check if the release branch already exists if (repository.Branches[releaseBranchName] != null) { this.stderr.WriteLine($"Cannot create branch '{releaseBranchName}' because it already exists."); throw new ReleasePreparationException(ReleasePreparationError.BranchAlreadyExists); } // create release branch and update version var releaseBranch = repository.CreateBranch(releaseBranchName); Commands.Checkout(repository, releaseBranch); this.UpdateVersion(projectDirectory, repository, versionOptions.Version, releaseVersion); this.stdout.WriteLine($"{releaseBranchName} branch now tracks v{releaseVersion} stabilization and release."); // update version on main branch Commands.Checkout(repository, originalBranchName); this.UpdateVersion(projectDirectory, repository, versionOptions.Version, nextDevVersion); this.stdout.WriteLine($"{originalBranchName} branch now tracks v{nextDevVersion} development."); // Merge release branch back to main branch var mergeOptions = new MergeOptions() { CommitOnSuccess = true, MergeFileFavor = MergeFileFavor.Ours, }; repository.Merge(releaseBranch, this.GetSignature(repository), mergeOptions); }
/// <summary> /// Initializes a new instance of the <see cref="VersionOracle"/> class. /// </summary> public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, LibGit2Sharp.Commit head, ICloudBuild cloudBuild, int?overrideVersionHeightOffset = null, string projectPathRelativeToGitRepoRoot = null) { var relativeRepoProjectDirectory = projectPathRelativeToGitRepoRoot ?? repo?.GetRepoRelativePath(projectDirectory); if (repo is object) { // If we're particularly git focused, normalize/reset projectDirectory to be the path we *actually* want to look at in case we're being redirected. projectDirectory = Path.Combine(repo.Info.WorkingDirectory, relativeRepoProjectDirectory); } var commit = head ?? repo?.Head.Tip; var committedVersion = VersionFile.GetVersion(commit, relativeRepoProjectDirectory); var workingVersion = head is object?VersionFile.GetVersion(head, relativeRepoProjectDirectory) : VersionFile.GetVersion(projectDirectory); if (overrideVersionHeightOffset.HasValue) { if (committedVersion != null) { committedVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } if (workingVersion != null) { workingVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } } this.VersionOptions = committedVersion ?? workingVersion; this.GitCommitId = commit?.Id.Sha ?? cloudBuild?.GitCommitId ?? null; this.GitCommitDate = commit?.Author.When; this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion); this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? repo?.Head.CanonicalName; // Override the typedVersion with the special build number and revision components, when available. if (repo != null) { this.Version = GetIdAsVersion(commit, committedVersion, workingVersion, this.VersionHeight); } else { this.Version = this.VersionOptions?.Version.Version ?? Version0; } // get the commit id abbreviation only if the commit id is set if (!string.IsNullOrEmpty(this.GitCommitId)) { var gitCommitIdShortFixedLength = this.VersionOptions?.GitCommitIdShortFixedLength ?? VersionOptions.DefaultGitCommitIdShortFixedLength; var gitCommitIdShortAutoMinimum = this.VersionOptions?.GitCommitIdShortAutoMinimum ?? 0; // get it from the git repository if there is a repository present and it is enabled if (repo != null && gitCommitIdShortAutoMinimum > 0) { this.GitCommitIdShort = repo.ObjectDatabase.ShortenObjectId(commit, gitCommitIdShortAutoMinimum); } else { this.GitCommitIdShort = this.GitCommitId.Substring(0, gitCommitIdShortFixedLength); } } this.VersionHeightOffset = this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0; this.PrereleaseVersion = this.ReplaceMacros(this.VersionOptions?.Version?.Prerelease ?? string.Empty); this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumberOrDefault ?? VersionOptions.CloudBuildNumberOptions.DefaultInstance; if (!string.IsNullOrEmpty(this.BuildingRef) && this.VersionOptions?.PublicReleaseRefSpec?.Count > 0) { this.PublicRelease = this.VersionOptions.PublicReleaseRefSpec.Any( expr => Regex.IsMatch(this.BuildingRef, expr)); } }
/// <summary> /// Initializes a new instance of the <see cref="VersionOracle"/> class. /// </summary> public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, LibGit2Sharp.Commit head, ICloudBuild cloudBuild, int?overrideVersionHeightOffset = null, string projectPathRelativeToGitRepoRoot = null) { var repoRoot = repo?.Info?.WorkingDirectory?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && repoRoot != null && repoRoot.StartsWith("\\") && (repoRoot.Length == 1 || repoRoot[1] != '\\')) { // We're in a worktree, which libgit2sharp only gives us as a path relative to the root of the assumed drive. // Add the drive: to the front of the repoRoot. repoRoot = repo.Info.Path.Substring(0, 2) + repoRoot; } var relativeRepoProjectDirectory = !string.IsNullOrWhiteSpace(repoRoot) ? (!string.IsNullOrEmpty(projectPathRelativeToGitRepoRoot) ? projectPathRelativeToGitRepoRoot : projectDirectory.Substring(repoRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) : null; var commit = head ?? repo?.Head.Tip; var committedVersion = VersionFile.GetVersion(commit, relativeRepoProjectDirectory); var workingVersion = head != null?VersionFile.GetVersion(head, relativeRepoProjectDirectory) : VersionFile.GetVersion(projectDirectory); if (overrideVersionHeightOffset.HasValue) { if (committedVersion != null) { committedVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } if (workingVersion != null) { workingVersion.VersionHeightOffset = overrideVersionHeightOffset.Value; } } this.VersionOptions = committedVersion ?? workingVersion; this.GitCommitId = commit?.Id.Sha ?? cloudBuild?.GitCommitId ?? null; this.GitCommitDate = commit?.Author.When; this.VersionHeight = CalculateVersionHeight(relativeRepoProjectDirectory, commit, committedVersion, workingVersion); this.BuildingRef = cloudBuild?.BuildingTag ?? cloudBuild?.BuildingBranch ?? repo?.Head.CanonicalName; // Override the typedVersion with the special build number and revision components, when available. if (repo != null) { this.Version = GetIdAsVersion(commit, committedVersion, workingVersion, this.VersionHeight); } else { this.Version = this.VersionOptions?.Version.Version ?? Version0; } // get the commit id abbreviation only if the commit id is set if (!string.IsNullOrEmpty(this.GitCommitId)) { var gitCommitIdShortFixedLength = this.VersionOptions?.GitCommitIdShortFixedLength ?? VersionOptions.DefaultGitCommitIdShortFixedLength; var gitCommitIdShortAutoMinimum = this.VersionOptions?.GitCommitIdShortAutoMinimum ?? 0; // get it from the git repository if there is a repository present and it is enabled if (repo != null && gitCommitIdShortAutoMinimum > 0) { this.GitCommitIdShort = repo.ObjectDatabase.ShortenObjectId(commit, gitCommitIdShortAutoMinimum); } else { this.GitCommitIdShort = this.GitCommitId.Substring(0, gitCommitIdShortFixedLength); } } this.VersionHeightOffset = this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0; this.PrereleaseVersion = this.ReplaceMacros(this.VersionOptions?.Version?.Prerelease ?? string.Empty); this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumberOrDefault ?? VersionOptions.CloudBuildNumberOptions.DefaultInstance; if (!string.IsNullOrEmpty(this.BuildingRef) && this.VersionOptions?.PublicReleaseRefSpec?.Length > 0) { this.PublicRelease = this.VersionOptions.PublicReleaseRefSpec.Any( expr => Regex.IsMatch(this.BuildingRef, expr)); } }