diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 9a1315605..27cd44f88 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -380,6 +380,9 @@ 85773E1E2A3E0A1F00C5D926 /* SettingsSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85773E1D2A3E0A1F00C5D926 /* SettingsSearchResult.swift */; }; 85CD0C5F2A10CC3200E531FD /* URL+isImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CD0C5E2A10CC3200E531FD /* URL+isImage.swift */; }; 85E4122A2A46C8CA00183F2B /* LocationsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E412292A46C8CA00183F2B /* LocationsSettings.swift */; }; + 8B46FD062B732DAA00573116 /* GitTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B46FD052B732DAA00573116 /* GitTag.swift */; }; + 8B46FD0A2B7521E300573116 /* SourceControlNavigatorAddTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B46FD092B7521E300573116 /* SourceControlNavigatorAddTagView.swift */; }; + 8E6377942B72C48D00E2403F /* GitClient+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6377932B72C48D00E2403F /* GitClient+Tag.swift */; }; 9D36E1BF2B5E7D7500443C41 /* GitBranchesGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D36E1BE2B5E7D7500443C41 /* GitBranchesGroup.swift */; }; B6041F4D29D7A4E9000F3454 /* SettingsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */; }; B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */; }; @@ -924,6 +927,9 @@ 85773E1D2A3E0A1F00C5D926 /* SettingsSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchResult.swift; sourceTree = ""; }; 85CD0C5E2A10CC3200E531FD /* URL+isImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+isImage.swift"; sourceTree = ""; }; 85E412292A46C8CA00183F2B /* LocationsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsSettings.swift; sourceTree = ""; }; + 8B46FD052B732DAA00573116 /* GitTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitTag.swift; sourceTree = ""; }; + 8B46FD092B7521E300573116 /* SourceControlNavigatorAddTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlNavigatorAddTagView.swift; sourceTree = ""; }; + 8E6377932B72C48D00E2403F /* GitClient+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitClient+Tag.swift"; sourceTree = ""; }; 9D36E1BE2B5E7D7500443C41 /* GitBranchesGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitBranchesGroup.swift; sourceTree = ""; }; B6041F4C29D7A4E9000F3454 /* SettingsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPageView.swift; sourceTree = ""; }; B6041F5129D7D6D6000F3454 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; @@ -2015,6 +2021,7 @@ B65B11032B09DB1C002852CF /* GitClient+Fetch.swift */, B607181C2B0C5BE3009CDAB4 /* GitClient+Stash.swift */, B628B7922B18369800F9775A /* GitClient+Validate.swift */, + 8E6377932B72C48D00E2403F /* GitClient+Tag.swift */, ); path = Client; sourceTree = ""; @@ -2023,6 +2030,7 @@ isa = PBXGroup; children = ( 587B9E5329301D8F00AC7927 /* GitCommit.swift */, + 8B46FD052B732DAA00573116 /* GitTag.swift */, 587B9E5429301D8F00AC7927 /* GitChangedFile.swift */, 587B9E5529301D8F00AC7927 /* GitType.swift */, 04BA7C0A2AE2A2D100584E1C /* GitBranch.swift */, @@ -2677,6 +2685,7 @@ children = ( B6C4F2A02B3CA37500B2B140 /* SourceControlNavigatorHistoryView.swift */, 20EBB504280C329800F3A5DA /* CommitListItemView.swift */, + 8B46FD092B7521E300573116 /* SourceControlNavigatorAddTagView.swift */, B6C4F2A22B3CA74800B2B140 /* CommitDetailsView.swift */, B6C4F2A82B3CB00100B2B140 /* CommitDetailsHeaderView.swift */, B6C4F2AB2B3CC4D000B2B140 /* CommitChangedFileListItemView.swift */, @@ -3354,6 +3363,7 @@ 6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */, 587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */, 6C91D57229B176FF0059A90D /* EditorManager.swift in Sources */, + 8B46FD062B732DAA00573116 /* GitTag.swift in Sources */, 6C82D6BC29C00CD900495C54 /* FirstResponderPropertyWrapper.swift in Sources */, 58D01C9B293167DC00C5B6B4 /* CodeEditKeychainConstants.swift in Sources */, B640A99E29E2184700715F20 /* SettingsForm.swift in Sources */, @@ -3387,6 +3397,7 @@ 6C7F37FE2A3EA6FA00217B83 /* View+focusedValue.swift in Sources */, B6C4F2A12B3CA37500B2B140 /* SourceControlNavigatorHistoryView.swift in Sources */, B6C6A43029771F7100A3D28F /* EditorTabBackground.swift in Sources */, + 8E6377942B72C48D00E2403F /* GitClient+Tag.swift in Sources */, B60718372B170638009CDAB4 /* SourceControlNavigatorRenameBranchView.swift in Sources */, 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */, B6F0517D29D9E4B100D72287 /* TerminalSettingsView.swift in Sources */, @@ -3554,6 +3565,7 @@ 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */, 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */, 6CFF967629BEBCD900182D6F /* FileCommands.swift in Sources */, + 8B46FD0A2B7521E300573116 /* SourceControlNavigatorAddTagView.swift in Sources */, B60718462B17DC15009CDAB4 /* RepoOutlineGroupItem.swift in Sources */, 613899B32B6E6FEE00A5CAF6 /* FuzzySearchable.swift in Sources */, B697937A29FF5668002027EC /* AccountsSettingsAccountLink.swift in Sources */, diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 09ed8554a..c4f37d9b6 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -264,6 +264,14 @@ final class CEWorkspaceFileManager { 0ドル.path == "\(self.folderUrl.relativePath)/.git/config" }) + let gitTagsChange = events.first(where: { + 0ドル.path == "\(self.folderUrl.relativePath)/.git/packed-refs" + }) + + let gitTagsChange2 = events.first(where: { + 0ドル.path.contains("\(self.folderUrl.relativePath)/.git/refs/tags") + }) + // If changes were made to project OR files were staged, refresh changes if !notGitChanges.isEmpty || gitIndexChange != nil { Task { @@ -305,6 +313,12 @@ final class CEWorkspaceFileManager { try await self.sourceControlManager?.validate() } } + + if gitTagsChange != nil || gitTagsChange2 != nil { + Task { + try await self.sourceControlManager?.refreshTags() + } + } } /// Creates or deletes children of the ``CEWorkspaceFile`` so that they are accurate with the file system, diff --git a/CodeEdit/Features/Git/Client/GitClient+Tag.swift b/CodeEdit/Features/Git/Client/GitClient+Tag.swift new file mode 100644 index 000000000..abe3e920e --- /dev/null +++ b/CodeEdit/Features/Git/Client/GitClient+Tag.swift @@ -0,0 +1,59 @@ +// +// GitClient+Tag.swift +// CodeEdit +// +// Created by Johnathan Baird on 2/6/24. +// + +import Foundation + +extension GitClient { + /// Determines if the current directory is a valid git repository. + /// + /// Runs `git rev-parse --is-inside-work-tree`. + /// + /// - Returns: True, if git finds a valid repository. + func createTag(tagName: String, commitHash: String, message: String?) async -> Bool { + do { + let output = try await run("tag -a \(tagName) \(commitHash) -m \(message ?? "")") + return output.trimmingCharacters(in: .whitespacesAndNewlines) == "true" + } catch { + return false + } + } + + func getTags() async throws -> [GitTag] { + do { + let output = try await run("for-each-ref refs/tags --format='%(refname:short) %(objectname) %(taggername) <%(taggeremail)> %(taggerdate)'") + var gitTags: [GitTag] = [] + let lines = output.components(separatedBy: "\n") + for line in lines where !line.isEmpty { + // First, separate the line by the known structure " ", + // which allows us to isolate the taggerName even if it contains spaces. + if let emailStartIndex = line.firstIndex(of: "<"), + let emailEndIndex = line.firstIndex(of: ">") { + let beforeEmail = line[..= 2 { + let name = beforeEmailComponents[0] + let hash = beforeEmailComponents[1] + let taggerName = beforeEmailComponents.dropFirst(2).joined(separator: " ") + let tag = GitTag(name: name, + hash: hash, + taggerName: taggerName, + taggerEmail: String(email).trimmingCharacters(in: CharacterSet(charactersIn: "")), + dateCreated: String(afterEmail.trimmingCharacters(in: .whitespaces).dropFirst(2))) + gitTags.append(tag) + } + } + } + print(gitTags) + return gitTags + } catch { + print("Failed to execute git command: \(error)") + return [] + } + } +} diff --git a/CodeEdit/Features/Git/Client/Models/GitTag.swift b/CodeEdit/Features/Git/Client/Models/GitTag.swift new file mode 100644 index 000000000..3a6f62518 --- /dev/null +++ b/CodeEdit/Features/Git/Client/Models/GitTag.swift @@ -0,0 +1,16 @@ +// +// GitTag.swift +// CodeEdit +// +// Created by Johnathan Baird on 2/6/24. +// + +import Foundation + +struct GitTag { + let name: String + let hash: String + let taggerName: String + let taggerEmail: String + let dateCreated: String +} diff --git a/CodeEdit/Features/Git/SourceControlManager.swift b/CodeEdit/Features/Git/SourceControlManager.swift index 56fff5e94..0a9623bbd 100644 --- a/CodeEdit/Features/Git/SourceControlManager.swift +++ b/CodeEdit/Features/Git/SourceControlManager.swift @@ -33,6 +33,9 @@ final class SourceControlManager: ObservableObject { /// All stash ebtrues @Published var stashEntries: [GitStashEntry] = [] + /// All Tags + @Published var tags: [GitTag] = [] + /// Number of unsynced commits with remote in current branch @Published var numberOfUnsyncedCommits: (ahead: Int, behind: Int) = (ahead: 0, behind: 0) @@ -332,6 +335,19 @@ final class SourceControlManager: ObservableObject { await showAlert(title: title, message: error.localizedDescription) } + /// Gets all tags + func refreshTags() async throws { + let tags = (try? await gitClient.getTags()) ?? [] + await MainActor.run { + self.tags = tags + } + } + + func newTag(tagName: String, commitHash: String, message: String?) async throws{ + try await gitClient.createTag(tagName: tagName, commitHash: commitHash, message: message ?? "") + try await refreshTags() + } + private func showAlert(title: String, message: String) async { await MainActor.run { let alert = NSAlert() diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift index 8a19f67e2..9c5a80402 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/CommitListItemView.swift @@ -8,11 +8,13 @@ import SwiftUI struct CommitListItemView: View { - + @EnvironmentObject var sourceControlManager: SourceControlManager var commit: GitCommit @Environment(\.openURL) private var openCommit + + @State var showNewTag: Bool = false init(commit: GitCommit) { self.commit = commit @@ -30,21 +32,37 @@ struct CommitListItemView: View { } Spacer() VStack(alignment: .trailing, spacing: 5) { - Text(commit.hash) - .font(.system(size: 10, design: .monospaced)) - .background( - RoundedRectangle(cornerRadius: 3) - .padding(.vertical, -1) - .padding(.horizontal, -2.5) - .foregroundColor(Color(nsColor: .quaternaryLabelColor)) - ) - .padding(.trailing, 2.5) + HStack { + if let matchingTag = sourceControlManager.tags.first(where: { 0ドル.hash == commit.commitHash }) { + (Text(Image(systemName: "tag")) + Text(matchingTag.name)) + .font(.system(size: 10, design: .monospaced)) + .background( + RoundedRectangle(cornerRadius: 3) + .padding(.vertical, -1) + .padding(.horizontal, -2.5) + .foregroundColor(Color(nsColor: .systemPurple)) + ) + .padding(.trailing, 2.5) + } + Text(commit.hash) + .font(.system(size: 10, design: .monospaced)) + .background( + RoundedRectangle(cornerRadius: 3) + .padding(.vertical, -1) + .padding(.horizontal, -2.5) + .foregroundColor(Color(nsColor: .quaternaryLabelColor)) + ) + .padding(.trailing, 2.5) + } Text(commit.date.relativeStringToNow()) .font(.system(size: 11)) .foregroundColor(.secondary) } .padding(.top, 1) } + .sheet(isPresented: $showNewTag) { + SourceControlNavigatorNewTagView(commitHash: commit.hash) + } .padding(.vertical, 1) .contentShape(Rectangle()) .contextMenu { @@ -67,8 +85,10 @@ struct CommitListItemView: View { Divider() } Group { - Button("Tag \(commit.hash)...") {} - .disabled(true) // TODO: Implementation Needed + Button("Tag \(commit.hash)...") { + showNewTag = true + } + // .disabled(true) // TODO: Implementation Needed Button("New Branch from \(commit.hash)...") {} .disabled(true) // TODO: Implementation Needed Button("Cherry-Pick \(commit.hash)...") {} diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorAddTagView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorAddTagView.swift new file mode 100644 index 000000000..d858122fb --- /dev/null +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/History/Views/SourceControlNavigatorAddTagView.swift @@ -0,0 +1,65 @@ +// +// SourceControlNavigatorAddTagView.swift +// CodeEdit +// +// Created by Johnathan Baird on 2/8/24. +// +import SwiftUI +struct SourceControlNavigatorNewTagView: View { + @EnvironmentObject var sourceControlManager: SourceControlManager + @Environment(\.dismiss) + var dismiss + + @State var name: String = "" + let commitHash: String + @State var message: String = "" + + func submit() { + Task { + do { + try await sourceControlManager.newTag(tagName: name, commitHash: commitHash, message: message) + await MainActor.run { + dismiss() + } + } catch { + await sourceControlManager.showAlertForError( + title: "Failed to create tag", + error: error + ) + } + } + } + + var body: some View { + VStack(spacing: 0) { + Form { + Section { + LabeledContent("Revision:", value: commitHash) + TextField("Tag:", text: $name) + TextField("Message:", text: $message) + } header: { + Text("Create a new tag from revision") + } + } + //.formStyle(.grouped) + .scrollDisabled(true) + .scrollContentBackground(.hidden) + .onSubmit { submit() } + HStack { + Spacer() + Button("Cancel") { + dismiss() + } + Button("Create") { + submit() + } + .buttonStyle(.borderedProminent) + .disabled(name.isEmpty) + } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 20) + } + .frame(maxWidth: 480) + } +} diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift index 7b698c315..cd18437f5 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Repository/Views/SourceControlNavigatorRepositoryView+outlineGroupData.swift @@ -26,6 +26,21 @@ extension SourceControlNavigatorRepositoryView { ) } ), + .init( + id: "TagsGroup", + label: "Tags", + systemImage: "tag.square.fill", + imageColor: Color(nsColor: .secondaryLabelColor), + children: sourceControlManager.tags.map { tag in + .init( + id: "Tag \(tag)", + label: tag.name, + description: tag.dateCreated, + systemImage: "tag", + imageColor: .purple + ) + } + ), .init( id: "StashedChangesGroup", label: "Stashed Changes", diff --git a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift index 627194128..bfd4cc7eb 100644 --- a/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift +++ b/CodeEdit/Features/NavigatorArea/SourceControlNavigator/Views/SourceControlNavigatorView.swift @@ -52,6 +52,7 @@ struct SourcControlNavigatorTabs: View { do { try await sourceControlManager.refreshRemotes() try await sourceControlManager.refreshStashEntries() + try await sourceControlManager.refreshTags() } catch { await sourceControlManager.showAlertForError(title: "Error refreshing Git data", error: error) }

AltStyle によって変換されたページ (->オリジナル) /