diff --git a/GitTfs.VsCommon/TfsHelper.Common.cs b/GitTfs.VsCommon/TfsHelper.Common.cs index 596f7468..3581f0b3 100644 --- a/GitTfs.VsCommon/TfsHelper.Common.cs +++ b/GitTfs.VsCommon/TfsHelper.Common.cs @@ -590,5 +590,11 @@ namespace Sep.Git.Tfs.VsCommon }); } + public virtual void CreateBranch(string sourcePath, string targetPath, int changesetId, string comment = null) + { + throw new NotImplementedException(); + + } + } } diff --git a/GitTfs.VsCommon/TfsHelper.PostVs2010.Common.cs b/GitTfs.VsCommon/TfsHelper.PostVs2010.Common.cs index ce7172bc..f8713ff1 100644 --- a/GitTfs.VsCommon/TfsHelper.PostVs2010.Common.cs +++ b/GitTfs.VsCommon/TfsHelper.PostVs2010.Common.cs @@ -119,6 +119,19 @@ namespace Sep.Git.Tfs.VsCommon merge.SourceItem.ChangeType + "': https://github.com/git-tfs/git-tfs/issues" }); } + + public override void CreateBranch(string sourcePath, string targetPath, int changesetId, string comment = null) + { + var changesetToBranch = new ChangesetVersionSpec(changesetId); + int branchChangesetId = VersionControl.CreateBranch(sourcePath, targetPath, changesetToBranch); + + if (comment != null) + { + Changeset changeset = VersionControl.GetChangeset(branchChangesetId); + changeset.Comment = comment; + changeset.Update(); + } + } } } diff --git a/GitTfs.VsFake/TfsHelper.VsFake.cs b/GitTfs.VsFake/TfsHelper.VsFake.cs index 4c505f44..6a815dba 100644 --- a/GitTfs.VsFake/TfsHelper.VsFake.cs +++ b/GitTfs.VsFake/TfsHelper.VsFake.cs @@ -366,6 +366,11 @@ namespace Sep.Git.Tfs.VsFake { throw new NotImplementedException(); } + + public void CreateBranch(string sourcePath, string targetPath, int changesetId, string comment = null) + { + throw new NotImplementedException(); + } #endregion } } diff --git a/GitTfs/Commands/Branch.cs b/GitTfs/Commands/Branch.cs index d546f9ad..a1cfdab3 100644 --- a/GitTfs/Commands/Branch.cs +++ b/GitTfs/Commands/Branch.cs @@ -1,4 +1,6 @@ +using System; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Collections.Generic; @@ -10,32 +12,184 @@ using StructureMap; namespace Sep.Git.Tfs.Commands { [Pluggable("branch")] - [Description("branch")] + [Description("branch\n\n" + + " * Display remote TFS branches:\n git tfs branch -r\n git tfs branch -r -all\n\n" + + " * Create a TFS branch from current commit:\n git tfs branch $/Repository/ProjectBranchToCreate --comment=\"Creation of my branch\"\n\n" + + " * Rename a remote branch:\n git tfs branch --move oldTfsRemoteName newTfsRemoteName\n\n" + + " * Delete a remote branche:\n git tfs branch --delete tfsRemoteName\n\n" + + " * Initialise an existing remote TFS branch:\n git tfs --init $/Repository/ProjectBranch\n git tfs --init $/Repository/ProjectBranch myNewBranch\n git tfs --init --all\n git tfs --init --tfs-parent-branch=$/Repository/ProjectParentBranch $/Repository/ProjectBranch\n")] [RequiresValidGitRepository] public class Branch : GitTfsCommand { private Globals globals; private TextWriter stdout; + private readonly Help helper; + private readonly Cleanup cleanup; + private readonly InitBranch initBranch; public bool DisplayRemotes { get; set; } + public bool ManageAll { get; set; } + public bool ShouldRenameRemote { get; set; } + public bool ShouldDeleteRemote { get; set; } + public bool ShouldInitBranch { get; set; } + public string Comment { get; set; } + public string TfsUsername { get; set; } + public string TfsPassword { get; set; } + public string AuthorsFilePath { get; set; } + public string ParentBranch { get; set; } public OptionSet OptionSet { get { return new OptionSet { - { "r|remotes", "Display all the TFS branch of the current TFS server", v => DisplayRemotes = (v != null) } + { "r|remotes", "Display the TFS branches of the current TFS root branch existing on the TFS server", v => DisplayRemotes = (v != null) }, + { "all", "Display (used with option --remotes) the TFS branches of all the root branches existing on the TFS server\n or Initialize (used with option --init) all existing TFS branches (For TFS 2010 and later)", v => ManageAll = (v != null) }, + { "comment=", "Comment used for the creation of the TFS branch ", v => Comment = v }, + { "m|move", "Rename a TFS remote", v => ShouldRenameRemote = (v != null) }, + { "delete", "Delete a TFS remote", v => ShouldDeleteRemote = (v != null) }, + { "init", "Initialize an existing TFS branch", v => ShouldInitBranch = (v != null) }, + { "b|tfs-parent-branch=", "TFS Parent branch of the TFS branch to clone (TFS 2008 only! And required!!) ex: $/Repository/ProjectParentBranch", v => ParentBranch = v }, + { "u|username=", "TFS username", v => TfsUsername = v }, + { "p|password=", "TFS password", v => TfsPassword = v }, + { "a|authors=", "Path to an Authors file to map TFS users to Git users", v => AuthorsFilePath = v }, } .Merge(globals.OptionSet); } } - public Branch(Globals globals, TextWriter stdout) + public Branch(Globals globals, TextWriter stdout, Help helper, Cleanup cleanup, InitBranch initBranch) { this.globals = globals; this.stdout = stdout; + this.helper = helper; + this.cleanup = cleanup; + this.initBranch = initBranch; + } + + public void SetInitBranchParameters() + { + initBranch.TfsUsername = TfsUsername; + initBranch.TfsPassword = TfsPassword; + initBranch.AuthorsFilePath = AuthorsFilePath; + initBranch.CloneAllBranches = ManageAll; + initBranch.ParentBranch = ParentBranch; + } + + public bool IsCommandWellUsed() + { + //Verify that some mutual exclusive options are not used together + return new[] {ShouldDeleteRemote, ShouldInitBranch, ShouldRenameRemote}.Count(b => b) <= 1; } public int Run() + { + if (!IsCommandWellUsed()) + return helper.Run(this); + + if (ShouldRenameRemote || ShouldDeleteRemote) + return helper.Run(this); + + if (ShouldInitBranch) + { + SetInitBranchParameters(); + return initBranch.Run(); + } + + return DisplayBranchData(); + } + + public int Run(string param) + { + if (!IsCommandWellUsed()) + return helper.Run(this); + + if (ShouldRenameRemote) + return helper.Run(this); + + if (ShouldInitBranch) + { + SetInitBranchParameters(); + return initBranch.Run(param); + } + + if (ShouldDeleteRemote) + return DeleteRemote(param); + + return CreateRemote(param); + } + + public int Run(string param1, string param2) + { + if (!IsCommandWellUsed()) + return helper.Run(this); + + if (ShouldDeleteRemote) + return helper.Run(this); + + if (ShouldInitBranch) + { + SetInitBranchParameters(); + return initBranch.Run(param1, param2); + } + + if (ShouldRenameRemote) + return RenameRemote(param1, param2); + + return CreateRemote(param1, param2); + } + + private int RenameRemote(string oldRemoteName, string newRemoteName) + { + var newRemoteNameExpected = globals.Repository.AssertValidBranchName(newRemoteName.ToGitRefName()); + if (newRemoteNameExpected != newRemoteName) + stdout.WriteLine("The name of the branch after renaming will be : " + newRemoteNameExpected); + + if (globals.Repository.HasRemote(newRemoteNameExpected)) + { + throw new GitTfsException("error: this remote name is already used!"); + } + + stdout.WriteLine("Cleaning before processing rename..."); + cleanup.Run(); + + globals.Repository.MoveRemote(oldRemoteName, newRemoteNameExpected); + + if(globals.Repository.RenameBranch(oldRemoteName, newRemoteName) == null) + stdout.WriteLine("warning: no local branch found to rename"); + + return GitTfsExitCodes.OK; + } + + private int CreateRemote(string tfsPath, string gitBranchNameExpected = null) + { + tfsPath.AssertValidTfsPath(); + Trace.WriteLine("Getting commit informations..."); + var commit = globals.Repository.GetCurrentTfsCommit(); + if(commit == null) + throw new GitTfsException("error : the current commit is not checked in TFS!"); + var remote = commit.Remote; + Trace.WriteLine("Creating branch in TFS..."); + remote.Tfs.CreateBranch(remote.TfsRepositoryPath, tfsPath, (int)commit.ChangesetId, Comment ?? "Creation branch " + tfsPath); + Trace.WriteLine("Init branch in local repository..."); + return initBranch.Run(tfsPath, gitBranchNameExpected); + } + + private int DeleteRemote(string remoteName) + { + var remote = globals.Repository.ReadTfsRemote(remoteName); + if (remote == null) + { + throw new GitTfsException(string.Format("Error: Remote \"{0}\" not found!", remoteName)); + } + + stdout.WriteLine("Cleaning before processing delete..."); + cleanup.Run(); + + globals.Repository.DeleteTfsRemote(remote); + return GitTfsExitCodes.OK; + } + + public int DisplayBranchData() { // should probably pull this from options so that it is settable from the command-line const string remoteId = GitTfsConstants.DefaultRepositoryId; @@ -43,11 +197,29 @@ namespace Sep.Git.Tfs.Commands var tfsRemotes = globals.Repository.ReadAllTfsRemotes(); if (DisplayRemotes) { - var remote = globals.Repository.ReadTfsRemote(remoteId); + if (!ManageAll) + { + var remote = globals.Repository.ReadTfsRemote(remoteId); - stdout.WriteLine("\nTFS branch structure:"); - WriteRemoteTfsBranchStructure(remote.Tfs, stdout, remote.TfsRepositoryPath, tfsRemotes); - return GitTfsExitCodes.OK; + stdout.WriteLine("\nTFS branch structure:"); + WriteRemoteTfsBranchStructure(remote.Tfs, stdout, remote.TfsRepositoryPath, tfsRemotes); + return GitTfsExitCodes.OK; + } + else + { + var remote = tfsRemotes.First(r => r.Id == remoteId); + if (!remote.Tfs.CanGetBranchInformation) + { + throw new GitTfsException("error: this version of TFS doesn't support this functionality"); + } + foreach (var branch in remote.Tfs.GetBranches().Where(b=>b.IsRoot)) + { + var root = remote.Tfs.GetRootTfsBranchForRemotePath(branch.Path); + var visitor = new WriteBranchStructureTreeVisitor(remote.TfsRepositoryPath, stdout, tfsRemotes); + root.AcceptVisitor(visitor); + } + return GitTfsExitCodes.OK; + } } WriteTfsRemoteDetails(stdout, tfsRemotes); @@ -58,6 +230,10 @@ namespace Sep.Git.Tfs.Commands { var root = tfsHelper.GetRootTfsBranchForRemotePath(tfsRepositoryPath); + if (!tfsHelper.CanGetBranchInformation) + { + throw new GitTfsException("error: this version of TFS doesn't support this functionality"); + } var visitor = new WriteBranchStructureTreeVisitor(tfsRepositoryPath, writer, tfsRemotes); root.AcceptVisitor(visitor); } @@ -114,4 +290,4 @@ namespace Sep.Git.Tfs.Commands } } } -} \ No newline at end of file +} diff --git a/GitTfs/Commands/InitBranch.cs b/GitTfs/Commands/InitBranch.cs index e70c9ffb..4745c973 100644 --- a/GitTfs/Commands/InitBranch.cs +++ b/GitTfs/Commands/InitBranch.cs @@ -26,14 +26,14 @@ namespace Sep.Git.Tfs.Commands public string TfsPassword { get; set; } public string ParentBranch { get; set; } public bool CloneAllBranches { get; set; } - string AuthorsFilePath { get; set; } + public string AuthorsFilePath { get; set; } public InitBranch(TextWriter stdout, Globals globals, Help helper, AuthorsFile authors) { - this._stdout = stdout; - this._globals = globals; - this._helper = helper; - this._authors = authors; + _stdout = stdout; + _globals = globals; + _helper = helper; + _authors = authors; } public OptionSet OptionSet @@ -87,7 +87,7 @@ namespace Sep.Git.Tfs.Commands if (defaultRemote.TfsRepositoryPath.ToLower() != rootBranch.Path.ToLower()) throw new GitTfsException(string.Format("error: Init all the branches is only possible when 'git tfs clone' was done from the trunk!!! Please clone again from '{0}'...", rootBranch.Path)); - var childBranchPaths = rootBranch.GetAllChildren().Select(b=>b.Path); + var childBranchPaths = rootBranch.GetAllChildren().Select(b=>b.Path).ToList(); _stdout.WriteLine("Tfs branches found:"); foreach (var tfsBranchPath in childBranchPaths) diff --git a/GitTfs/Core/DerivedGitTfsRemote.cs b/GitTfs/Core/DerivedGitTfsRemote.cs index b208a482..ef5b9df8 100644 --- a/GitTfs/Core/DerivedGitTfsRemote.cs +++ b/GitTfs/Core/DerivedGitTfsRemote.cs @@ -233,5 +233,11 @@ namespace Sep.Git.Tfs.Core } #endregion + + + public RemoteInfo RemoteInfo + { + get { throw new NotImplementedException(); } + } } } diff --git a/GitTfs/Core/GitRepository.cs b/GitTfs/Core/GitRepository.cs index 8a87cc87..59444cfb 100644 --- a/GitTfs/Core/GitRepository.cs +++ b/GitTfs/Core/GitRepository.cs @@ -8,6 +8,7 @@ using Sep.Git.Tfs.Commands; using Sep.Git.Tfs.Core.TfsInterop; using StructureMap; using LibGit2Sharp; +using Branch = LibGit2Sharp.Branch; namespace Sep.Git.Tfs.Core { @@ -136,6 +137,59 @@ namespace Sep.Git.Tfs.Core return _cachedRemotes[remote.Id] = gitTfsRemote; } + public void DeleteTfsRemote(IGitTfsRemote remote) + { + if (remote == null) + throw new GitTfsException("error: the name of the remote to delete is invalid!"); + + UnsetTfsRemoteConfig(remote.Id); + _repository.Refs.Remove(remote.RemoteRef); + } + + private void UnsetTfsRemoteConfig(string remoteId) + { + foreach (var entry in _remoteConfigReader.Delete(remoteId)) + { + _repository.Config.Unset(entry.Key); + } + _cachedRemotes = null; + } + + public void MoveRemote(string oldRemoteName, string newRemoteName) + { + if (!_repository.Refs.IsValidName("refs/heads/" + oldRemoteName)) + throw new GitTfsException("error: the name of the remote to move is invalid!"); + + if (!_repository.Refs.IsValidName("refs/heads/" + newRemoteName)) + throw new GitTfsException("error: the new name of the remote is invalid!"); + + if (HasRemote(newRemoteName)) + throw new GitTfsException(string.Format("error: this remote name \"{0}\" is already used!", newRemoteName)); + + var oldRemote = ReadTfsRemote(oldRemoteName); + if(oldRemote == null) + throw new GitTfsException(string.Format("error: the remote \"{0}\" doesn't exist!", oldRemoteName)); + + var remoteInfo = oldRemote.RemoteInfo; + remoteInfo.Id = newRemoteName; + + CreateTfsRemote(remoteInfo); + var newRemote = ReadTfsRemote(newRemoteName); + + _repository.Refs.Move(oldRemote.RemoteRef, newRemote.RemoteRef); + UnsetTfsRemoteConfig(oldRemoteName); + } + + public Branch RenameBranch(string oldName, string newName) + { + var branch = _repository.Branches[oldName]; + + if (branch == null) + return null; + + return _repository.Branches.Move(branch, newName); + } + private IDictionary ReadTfsRemotes() { // does this need to ensuretfsauthenticated? @@ -206,6 +260,12 @@ namespace Sep.Git.Tfs.Core return tfsCommits; } + public TfsChangesetInfo GetCurrentTfsCommit() + { + var currentCommit = _repository.Head.Commits.First(); + return TryParseChangesetInfo(currentCommit.Message, currentCommit.Sha, false); + } + private void FindTfsCommits(TextReader stdout, ICollection tfsCommits, bool includeStubRemotes) { string currentCommit = null; diff --git a/GitTfs/Core/GitTfsRemote.cs b/GitTfs/Core/GitTfsRemote.cs index 6972d467..07046784 100644 --- a/GitTfs/Core/GitTfsRemote.cs +++ b/GitTfs/Core/GitTfsRemote.cs @@ -20,6 +20,7 @@ namespace Sep.Git.Tfs.Core private long? maxChangesetId; private string maxCommitHash; private bool isTfsAuthenticated; + public RemoteInfo RemoteInfo { get; private set; } public GitTfsRemote(RemoteInfo info, IGitRepository repository, RemoteOptions remoteOptions, Globals globals, ITfsHelper tfsHelper, TextWriter stdout) { @@ -29,6 +30,7 @@ namespace Sep.Git.Tfs.Core Tfs = tfsHelper; Repository = repository; + RemoteInfo = info; Id = info.Id; TfsUrl = info.Url; TfsRepositoryPath = info.Repository; diff --git a/GitTfs/Core/IGitRepository.cs b/GitTfs/Core/IGitRepository.cs index 5369a558..bec2321a 100644 --- a/GitTfs/Core/IGitRepository.cs +++ b/GitTfs/Core/IGitRepository.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Sep.Git.Tfs.Commands; +using Branch = LibGit2Sharp.Branch; namespace Sep.Git.Tfs.Core { @@ -12,11 +13,13 @@ namespace Sep.Git.Tfs.Core IEnumerable ReadAllTfsRemotes(); IGitTfsRemote ReadTfsRemote(string remoteId); IGitTfsRemote CreateTfsRemote(RemoteInfo remoteInfo); + void DeleteTfsRemote(IGitTfsRemote remoteId); bool HasRemote(string remoteId); bool HasRef(string gitRef); void MoveTfsRefForwardIfNeeded(IGitTfsRemote remote); IEnumerable GetLastParentTfsCommits(string head); IEnumerable GetLastParentTfsCommits(string head, bool includeStubRemotes); + TfsChangesetInfo GetCurrentTfsCommit(); IDictionary GetObjects(string commit); string HashAndInsertObject(string filename); IEnumerable GetChangedFiles(string from, string to); @@ -27,8 +30,10 @@ namespace Sep.Git.Tfs.Core string GetCommitMessage(string head, string parentCommitish); string AssertValidBranchName(string gitBranchName); bool CreateBranch(string gitBranchName, string target); + Branch RenameBranch(string oldName, string newName); string FindCommitHashByCommitMessage(string patternToFind); void CreateTag(string name, string sha, string comment, string Owner, string emailOwner, System.DateTime creationDate); void CreateNote(string sha, string content, string owner, string emailOwner, DateTime creationDate); + void MoveRemote(string oldRemoteName, string newRemoteName); } } diff --git a/GitTfs/Core/IGitTfsRemote.cs b/GitTfs/Core/IGitTfsRemote.cs index 09e8091f..942c6cea 100644 --- a/GitTfs/Core/IGitTfsRemote.cs +++ b/GitTfs/Core/IGitTfsRemote.cs @@ -8,6 +8,7 @@ namespace Sep.Git.Tfs.Core public interface IGitTfsRemote { bool IsDerived { get; } + RemoteInfo RemoteInfo { get; } string Id { get; set; } string TfsUrl { get; set; } string TfsRepositoryPath { get; set; } diff --git a/GitTfs/Core/RemoteConfigConverter.cs b/GitTfs/Core/RemoteConfigConverter.cs index 0dbb0d33..d07183a3 100644 --- a/GitTfs/Core/RemoteConfigConverter.cs +++ b/GitTfs/Core/RemoteConfigConverter.cs @@ -36,7 +36,7 @@ namespace Sep.Git.Tfs.Core remote.Autotag = bool.Parse(entry.Value); } } - return remotes.Values; + return remotes.Values.Where(r => !string.IsNullOrWhiteSpace(r.Url) && !string.IsNullOrWhiteSpace(r.Repository)); } public IEnumerable> Dump(RemoteInfo remote) @@ -58,5 +58,13 @@ namespace Sep.Git.Tfs.Core { return new KeyValuePair(key, value); } + + public IEnumerable> Delete(string remoteId) + { + if (string.IsNullOrWhiteSpace(remoteId)) + return new List>(); + + return Dump(new RemoteInfo {Id = remoteId}); + } } } diff --git a/GitTfs/Core/TfsInterop/ITfsHelper.cs b/GitTfs/Core/TfsInterop/ITfsHelper.cs index 26bcb2a1..8ed4434a 100644 --- a/GitTfs/Core/TfsInterop/ITfsHelper.cs +++ b/GitTfs/Core/TfsInterop/ITfsHelper.cs @@ -32,5 +32,6 @@ namespace Sep.Git.Tfs.Core.TfsInterop IEnumerable GetAllTfsRootBranchesOrderedByCreation(); IEnumerable GetBranches(); void EnsureAuthenticated(); + void CreateBranch(string sourcePath, string targetPath, int changesetId, string comment = null); } } diff --git a/GitTfsTest/Core/RemoteConfigConverterTests.cs b/GitTfsTest/Core/RemoteConfigConverterTests.cs index d2e5e417..45c66bed 100644 --- a/GitTfsTest/Core/RemoteConfigConverterTests.cs +++ b/GitTfsTest/Core/RemoteConfigConverterTests.cs @@ -134,6 +134,24 @@ namespace Sep.Git.Tfs.Test.Core Assert.Equal(new string[] { "http://old:8080/", "http://other/" }, remote.Aliases); Assert.True(remote.Autotag); } + + + [Fact] + public void ShouldNotReturnLackingTfsUrlRemote() + { + var remotes = Load( + c("tfs-remote.default.repository", "$/project")); + Assert.Equal(0, remotes.Count()); + } + + [Fact] + public void ShouldNotReturnLackingTfsRepositoryRemote() + { + var remotes = Load( + c("tfs-remote.default.url", "http://server/path")); + Assert.Equal(0, remotes.Count()); + } + } RemoteConfigConverter _converter = new RemoteConfigConverter();