-
Notifications
You must be signed in to change notification settings - Fork 0
Add commands for managing git tags #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
295540c
2b58549
a78dc87
557ceae
b7da829
a0b3b42
9ed9789
79f97e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| namespace CreativeCoders.Git.Abstractions.GitCommands; | ||
|
|
||
| public class FetchTagsCommandOptions | ||
| { | ||
| public bool Prune { get; set; } = true; | ||
|
|
||
| public string RemoteName { get; set; } = "origin"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace CreativeCoders.Git.Abstractions.GitCommands; | ||
|
|
||
| public interface IFetchTagsCommand | ||
| { | ||
| void Execute(FetchTagsCommandOptions commandOptions); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using CreativeCoders.Git.Abstractions.GitCommands; | ||
|
|
||
| namespace CreativeCoders.Git.GitCommands; | ||
|
|
||
| internal class FetchTagsCommand(RepositoryContext repositoryContext) : IFetchTagsCommand | ||
| { | ||
| private readonly RepositoryContext _repositoryContext = Ensure.NotNull(repositoryContext); | ||
|
|
||
| public void Execute(FetchTagsCommandOptions commandOptions) | ||
| { | ||
| var fetchOptions = new FetchOptions | ||
| { | ||
| Prune = commandOptions.Prune, | ||
| TagFetchMode = TagFetchMode.All | ||
| }; | ||
|
|
||
| Commands.Fetch(_repositoryContext.LibGitRepository, commandOptions.RemoteName, ["+refs/tags/*:refs/tags/*"], | ||
| fetchOptions, | ||
| "Fetch all tags"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,11 @@ internal GitTag(Tag tag) | |
| _tag = Ensure.NotNull(tag); | ||
|
|
||
| Name = new ReferenceName(_tag.CanonicalName); | ||
|
|
||
| if (_tag.Target is Commit commit) | ||
| { | ||
| TargetCommit = GitCommit.From(commit); | ||
| } | ||
| } | ||
|
Comment on lines
+18
to
22
|
||
|
|
||
| static GitTag() => InitComparableObject(x => x.Name.Canonical); | ||
|
|
@@ -34,5 +39,7 @@ internal GitTag(Tag tag) | |
| return GitCommit.From(target as Commit); | ||
| } | ||
|
|
||
| public IGitCommit? TargetCommit { get; } | ||
|
|
||
| public static implicit operator Tag(GitTag tag) => tag._tag; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,37 +19,62 @@ internal GitTagCollection(RepositoryContext context) | |||||
| _libGitCaller = _context.LibGitCaller; | ||||||
| } | ||||||
|
|
||||||
| public IGitTag CreateTag(string tagName) | ||||||
| public IGitTag CreateTag(string tagName, string? objectish = null) | ||||||
| { | ||||||
| return new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName))); | ||||||
| return string.IsNullOrWhiteSpace(objectish) | ||||||
| ? new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName))) | ||||||
| : new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName, objectish))); | ||||||
| } | ||||||
|
|
||||||
| public IGitTag CreateTag(string tagName, string objectish) | ||||||
| public IGitTag CreateTagWithMessage(string tagName, string message, string? objectish = null) | ||||||
| { | ||||||
| return new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName, objectish))); | ||||||
| return string.IsNullOrWhiteSpace(objectish) | ||||||
| ? new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName, _context.GetSignature(), message))) | ||||||
| : new GitTag(_libGitCaller | ||||||
| .Invoke(() => _repository.ApplyTag(tagName, objectish, _context.GetSignature(), message))); | ||||||
| } | ||||||
|
|
||||||
| public IGitTag CreateTagWithMessage(string tagName, string message) | ||||||
| public void DeleteTag(string tagName, bool deleteOnRemote = false) | ||||||
| { | ||||||
| return new GitTag(_libGitCaller.Invoke(() => _repository.ApplyTag(tagName, _context.GetSignature(), message))); | ||||||
| _libGitCaller.Invoke(() => _repository.Tags.Remove(tagName)); | ||||||
|
|
||||||
| if (deleteOnRemote) | ||||||
| { | ||||||
| DeleteRemoteTag(tagName); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| public IGitTag CreateTagWithMessage(string tagName, string objectish, string message) | ||||||
| public void DeleteTag(IGitTag tag, bool deleteOnRemote = false) | ||||||
| { | ||||||
| return new GitTag( | ||||||
| _libGitCaller | ||||||
| .Invoke(() => _repository.ApplyTag(tagName, objectish, _context.GetSignature(), message))); | ||||||
| DeleteTag(tag.Name.Canonical, deleteOnRemote); | ||||||
|
||||||
| DeleteTag(tag.Name.Canonical, deleteOnRemote); | |
| DeleteTag(tag.Name.Friendly, deleteOnRemote); |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PushTag(string tagName) forwards tagName directly to Network.Push(...) as the refspec. Given the method name/signature, callers are likely to pass a friendly tag name (e.g. v1.2.3), not a canonical refspec (refs/tags/v1.2.3). Either normalize the input to a proper tag refspec before pushing, or rename/ document the parameter as a refspec to avoid subtle push failures.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| namespace CreativeCoders.GitTool.Cli.Commands.Shared; | ||
|
|
||
| public static class BoolExtensions | ||
| { | ||
| public static void IfElse(this bool condition, Action trueAction, Action falseAction) | ||
| { | ||
| if (condition) | ||
| { | ||
| trueAction(); | ||
| } | ||
| else | ||
| { | ||
| falseAction(); | ||
| } | ||
| } | ||
|
|
||
| public static T IfElse<T>(this bool condition, Func<T> trueFunc, Func<T> falseFunc) | ||
| { | ||
| return condition ? trueFunc() : falseFunc(); | ||
| } | ||
|
|
||
| public static bool If(this bool condition, Action action) | ||
| { | ||
| if (!condition) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| action(); | ||
| return true; | ||
| } | ||
|
|
||
| public static void Else(this bool condition, Action action) | ||
| { | ||
| if (condition) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| action(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| using CreativeCoders.Cli.Core; | ||
| using CreativeCoders.Core; | ||
| using CreativeCoders.Git.Abstractions; | ||
| using CreativeCoders.Git.Abstractions.Tags; | ||
| using CreativeCoders.SysConsole.Core; | ||
| using JetBrains.Annotations; | ||
| using Spectre.Console; | ||
|
|
||
| namespace CreativeCoders.GitTool.Cli.Commands.TagGroup.Create; | ||
|
|
||
| [UsedImplicitly] | ||
| [CliCommand([TagCommandGroup.Name, "create"], Description = "Creates a new tag")] | ||
| public class CreateTagCommand(IAnsiConsole ansiConsole, IGitRepository gitRepository) : ICliCommand<CreateTagOptions> | ||
| { | ||
| private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); | ||
|
|
||
| private readonly IGitRepository _gitRepository = Ensure.NotNull(gitRepository); | ||
|
|
||
| public Task<CommandResult> ExecuteAsync(CreateTagOptions options) | ||
| { | ||
| _ansiConsole.WriteLine($"Creating tag '{options.TagName}'..."); | ||
|
|
||
| var tag = CreateTag(options); | ||
|
|
||
| _ansiConsole.MarkupLines( | ||
| $"Tag '{tag.Name.Friendly}' created.".ToSuccessMarkup(), | ||
| string.Empty, | ||
| $"Target commit: {tag.TargetCommit?.Id.Sha ?? "[none]"}".ToEscapedMarkup()); | ||
|
|
||
| if (options.PushAfterCreate) | ||
| { | ||
| _ansiConsole.WriteLines( | ||
| string.Empty, | ||
| "Pushing tag after creation"); | ||
|
|
||
| _gitRepository.Tags.PushTag(tag); | ||
|
|
||
| _ansiConsole.MarkupLine("Tag pushed.".ToSuccessMarkup()); | ||
| } | ||
|
|
||
| return Task.FromResult(CommandResult.Success); | ||
| } | ||
|
|
||
| private IGitTag CreateTag(CreateTagOptions options) | ||
| { | ||
| return string.IsNullOrWhiteSpace(options.Message) | ||
| ? _gitRepository.Tags.CreateTag(options.TagName, options.Objectish) | ||
| : _gitRepository.Tags.CreateTagWithMessage(options.TagName, options.Message, options.Objectish); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| using CreativeCoders.SysConsole.Cli.Parsing; | ||
| using JetBrains.Annotations; | ||
|
|
||
| namespace CreativeCoders.GitTool.Cli.Commands.TagGroup.Create; | ||
|
|
||
| [PublicAPI] | ||
| public class CreateTagOptions | ||
| { | ||
| [OptionValue(0, IsRequired = true)] public string TagName { get; set; } = string.Empty; | ||
|
|
||
| [OptionParameter('p', "push", HelpText = "Push tag after creation")] | ||
| public bool PushAfterCreate { get; set; } | ||
|
|
||
| [OptionParameter('m', "message", HelpText = "Message for tag")] | ||
| public string? Message { get; set; } | ||
|
|
||
| [OptionParameter('o', "objectish", HelpText = "Object to tag")] | ||
| public string? Objectish { get; set; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FetchOptionshere don't set aCredentialsProvider. The repository’s existing fetch path usesgitFetchOptions.ToFetchOptions(GetCredentialsHandler()), so this new command may fail against authenticated remotes. SetfetchOptions.CredentialsProviderusing the repository context’s credentials handler (and keep behavior consistent with other fetch operations).