Add commands for managing git tags#25
Conversation
- Introduce `TagCommandGroup` as a command group for tag-related commands. - Implement `ListTagsCommand` with options to display tags in simple or detailed views. - Add support for `TargetCommit` property in `IGitTag` and `GitTag` to enable sorting and extended tag details. - Create `BoolExtensions` helper class for streamlined boolean condition handling in commands.
- Introduced `IFetchTagsCommand` interface and `FetchTagsCommand` implementation for fetching tags. - Added CLI implementation for fetching tags under `TagGroup`. - Updated project structure and `GitCommands` to include the fetch tags functionality.
- Add `CreateTagCommand` for creating new tags with additional options. - Introduce `FetchTagsCommandOptions` for flexible tag fetching configuration. - Update `FetchTagsCommand` to use `FetchTagsCommandOptions`. - Simplify `IGitTagCollection` methods by combining similar tag creation methods. - Remove redundant console writes across various commands.
- Update `FetchTagsCommand` to use markup for success message.
- Implement CLI command for deleting tags with options for remote deletion. - Extend `IGitTagCollection` and `GitTagCollection` with methods for tag removal. - Update project structure to include `TagGroup/Delete` commands.
…elated CLI option classes and add `[UsedImplicitly]` for `FetchTagsCommand`.
There was a problem hiding this comment.
Pull request overview
This PR adds a new tag command group to the GitTool CLI, introducing commands to list/create/delete/fetch git tags, and extends the underlying Git abstractions to support tag management operations (including fetching tags via libgit2sharp).
Changes:
- Add
tagCLI command group withlist,create,delete, andfetchsubcommands. - Extend
IGitTagCollection/GitTagCollectionto support tag creation withobjectish, deletion (incl. remote), and pushing by name. - Add a new
IFetchTagsCommandimplementation and wire it intoIGitCommands.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| source/GitTool/CreativeCoders.GitTool.Cli.GtApp/Program.cs | Adjust CLI host footer output behavior. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/TagCommandGroup.cs | Introduce tag command group metadata. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/List/ListTagsOptions.cs | Add options for tag listing output format. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/List/ListTagsCommand.cs | Implement tag list command. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Fetch/FetchTagsOptions.cs | Add options for tag fetch. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Fetch/FetchTagsCommand.cs | Implement tag fetch command. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagOptions.cs | Add options for tag delete. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagCommand.cs | Implement tag delete command. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Create/CreateTagOptions.cs | Add options for tag create. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Create/CreateTagCommand.cs | Implement tag create command. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/Shared/GitToolPushCommand.cs | Remove trailing blank-line output. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/Shared/GitToolPullCommand.cs | Remove trailing blank-line output. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/Shared/BoolExtensions.cs | Add bool chaining helpers used by new tag commands. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs | Update tag creation call to match new signature. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/BranchGroup/Update/UpdateBranchesCommand.cs | Remove extra empty-line output. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/BranchGroup/List/ListBranchesCommand.cs | Remove extra empty-line output. |
| source/GitTool/CreativeCoders.GitTool.Cli.Commands/BranchGroup/Info/InfoBranchesCommand.cs | Remove extra empty-line output. |
| source/Git/CreativeCoders.Git/Tags/GitTagCollection.cs | Add tag create/delete/push APIs and remote tag deletion logic. |
| source/Git/CreativeCoders.Git/Tags/GitTag.cs | Add TargetCommit property population for tags. |
| source/Git/CreativeCoders.Git/GitCommands/GitCommands.cs | Add factory method for fetch-tags command. |
| source/Git/CreativeCoders.Git/GitCommands/FetchTagsCommand.cs | Implement fetch-tags command via libgit2sharp. |
| source/Git/CreativeCoders.Git.Abstractions/Tags/IGitTagCollection.cs | Update tag collection interface for new tag operations. |
| source/Git/CreativeCoders.Git.Abstractions/Tags/IGitTag.cs | Add TargetCommit to tag abstraction. |
| source/Git/CreativeCoders.Git.Abstractions/GitCommands/IGitCommands.cs | Add CreateFetchTagsCommand to command factory abstraction. |
| source/Git/CreativeCoders.Git.Abstractions/GitCommands/IFetchTagsCommand.cs | Add abstraction for fetching tags. |
| source/Git/CreativeCoders.Git.Abstractions/GitCommands/FetchTagsCommandOptions.cs | Add options DTO for fetching tags. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (_tag.Target is Commit commit) | ||
| { | ||
| TargetCommit = GitCommit.From(commit); | ||
| } | ||
| } |
There was a problem hiding this comment.
TargetCommit is only populated when the tag directly targets a Commit (lightweight tags). For annotated tags (TagAnnotation), TargetCommit stays null even though PeeledTargetCommit() can resolve the commit, which will break tag sorting / display in the new CLI commands. Consider setting TargetCommit based on the peeled target (or implement TargetCommit to return PeeledTargetCommit()), so both lightweight and annotated tags have a commit when available.
| return new GitTag( | ||
| _libGitCaller | ||
| .Invoke(() => _repository.ApplyTag(tagName, objectish, _context.GetSignature(), message))); | ||
| DeleteTag(tag.Name.Canonical, deleteOnRemote); |
There was a problem hiding this comment.
DeleteTag(IGitTag ...) passes tag.Name.Canonical into DeleteTag(string ...). Since canonical tag names include the refs/tags/ prefix, this can produce an invalid remote delete refspec (:refs/tags/refs/tags/<name>) when deleteOnRemote is true. Pass tag.Name.Friendly (or otherwise normalize to a plain tag name) to avoid duplicating the prefix.
| DeleteTag(tag.Name.Canonical, deleteOnRemote); | |
| DeleteTag(tag.Name.Friendly, deleteOnRemote); |
| public void PushTag(string tagName) | ||
| { | ||
| var pushOptions = new PushOptions | ||
| { | ||
| CredentialsProvider = _context.GetCredentialsHandler() | ||
| }; | ||
|
|
||
| _libGitCaller.Invoke(() => | ||
| _repository.Network.Push(_repository.Network.Remotes[GitRemotes.Origin], tag.Name.Canonical, pushOptions)); | ||
| _repository.Network.Push(_repository.Network.Remotes[GitRemotes.Origin], tagName, pushOptions)); | ||
| } |
There was a problem hiding this comment.
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.
| Prune = commandOptions.Prune, | ||
| TagFetchMode = TagFetchMode.All | ||
| }; | ||
|
|
There was a problem hiding this comment.
FetchOptions here don't set a CredentialsProvider. The repository’s existing fetch path uses gitFetchOptions.ToFetchOptions(GetCredentialsHandler()), so this new command may fail against authenticated remotes. Set fetchOptions.CredentialsProvider using the repository context’s credentials handler (and keep behavior consistent with other fetch operations).
| fetchOptions.CredentialsProvider = _repositoryContext.GetCredentialsHandler(); |
| var remotes = _gitRepository.Remotes.ToList(); | ||
|
|
||
| if (remotes.Count == 0) | ||
| { | ||
| _ansiConsole.WriteLine("No remote repository found."); | ||
| return Task.FromResult(CommandResult.Success); | ||
| } | ||
|
|
||
| var fetchTagsCommand = _gitRepository.Commands.CreateFetchTagsCommand(); | ||
|
|
||
| foreach (var remote in remotes) | ||
| { | ||
| _ansiConsole.WriteLine($"Fetch tags from remote '{remote.Name}' ({remote.Url})..."); | ||
| fetchTagsCommand.Execute(new FetchTagsCommandOptions | ||
| { | ||
| RemoteName = remote.Name, | ||
| Prune = options.Prune | ||
| }); | ||
| } |
There was a problem hiding this comment.
FetchTagsOptions.RemoteName is never used. The command currently fetches tags from all remotes (looping over remotes) regardless of the --remote option, so the CLI behavior doesn't match the option/help text. Use options.RemoteName to select a single remote (and report an error if not found), or remove the option if fetching all remotes is intended.
| public string RemoteName { get; set; } = "origin"; | ||
|
|
||
| [OptionParameter('p', "prune", HelpText = "Prune deleted remote tags")] | ||
| public bool Prune { get; set; } = true; |
There was a problem hiding this comment.
Prune defaults to true while also being a boolean flag option. With most CLI parsers, a flag can only turn the value on, so this makes pruning effectively always enabled and prevents users from disabling it. Consider defaulting to false (so --prune enables it) or adding an explicit --no-prune option / using a nullable bool if the parser supports it.
| public bool Prune { get; set; } = true; | |
| public bool Prune { get; set; } = false; |
| [OptionValue(0, HelpText = "The name of the tag to delete")] | ||
| public string TagName { get; set; } = string.Empty; |
There was a problem hiding this comment.
The positional TagName argument isn't marked as required and defaults to string.Empty. If the user runs tag delete without a name, this will attempt to delete an empty tag name (likely throwing) instead of failing fast with a clear validation error. Mark the option value as required (and avoid using an empty-string default).
| [OptionValue(0, HelpText = "The name of the tag to delete")] | |
| public string TagName { get; set; } = string.Empty; | |
| [OptionValue(0, HelpText = "The name of the tag to delete", Required = true)] | |
| public string TagName { get; set; } = null!; |
| var tags = _gitRepository.Tags.OrderByDescending(x => | ||
| x.TargetCommit?.Author.When ?? DateTimeOffset.MinValue); | ||
|
|
There was a problem hiding this comment.
This command sorts and displays tag metadata using IGitTag.TargetCommit. As implemented, TargetCommit is null for annotated tags, which will cause annotated tags to sort to the bottom and show empty date/committer info. Either use PeeledTargetCommit() here, or ensure TargetCommit resolves the peeled commit for annotated tags.
No description provided.