Skip to content

Commit 409c4ce

Browse files
committed
Author name, author email, and prefer rebase when pulling settings use the users global git config instead of CodeEdit settings. Introduced Limiter.debounce and Limiter.throttle.
1 parent b7c9797 commit 409c4ce

File tree

8 files changed

+210
-22
lines changed

8 files changed

+210
-22
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@
554554
B67DB0F62AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0F52AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift */; };
555555
B67DB0F92AFDF638002DC647 /* IconButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0F82AFDF638002DC647 /* IconButtonStyle.swift */; };
556556
B67DB0FC2AFDF71F002DC647 /* IconToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DB0FB2AFDF71F002DC647 /* IconToggleStyle.swift */; };
557+
B67DBB882CD51C55007F4F18 /* Limiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67DBB872CD51C51007F4F18 /* Limiter.swift */; };
557558
B68108042C60287F008B27C1 /* StartTaskToolbarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68108032C60287F008B27C1 /* StartTaskToolbarButton.swift */; };
558559
B685DE7929CC9CCD002860C8 /* StatusBarIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685DE7829CC9CCD002860C8 /* StatusBarIcon.swift */; };
559560
B6966A282C2F683300259C2D /* SourceControlPullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6966A272C2F683300259C2D /* SourceControlPullView.swift */; };
@@ -585,6 +586,7 @@
585586
B6CFD8112C20A8EE00E63F1A /* NSFont+WithWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */; };
586587
B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA582971078500301FAC /* InspectorSection.swift */; };
587588
B6D7EA5C297107DD00301FAC /* InspectorField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D7EA5B297107DD00301FAC /* InspectorField.swift */; };
589+
B6E38E022CD3E63A00F4E65A /* GitConfigClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */; };
588590
B6E41C7029DD157F0088F9F4 /* AccountsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */; };
589591
B6E41C7429DD40010088F9F4 /* View+HideSidebarToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */; };
590592
B6E41C7929DE02800088F9F4 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */; };
@@ -1217,6 +1219,7 @@
12171219
B67DB0F52AFC2A7A002DC647 /* FindNavigatorToolbarBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorToolbarBottom.swift; sourceTree = "<group>"; };
12181220
B67DB0F82AFDF638002DC647 /* IconButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButtonStyle.swift; sourceTree = "<group>"; };
12191221
B67DB0FB2AFDF71F002DC647 /* IconToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconToggleStyle.swift; sourceTree = "<group>"; };
1222+
B67DBB872CD51C51007F4F18 /* Limiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Limiter.swift; sourceTree = "<group>"; };
12201223
B68108032C60287F008B27C1 /* StartTaskToolbarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTaskToolbarButton.swift; sourceTree = "<group>"; };
12211224
B685DE7829CC9CCD002860C8 /* StatusBarIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarIcon.swift; sourceTree = "<group>"; };
12221225
B6966A272C2F683300259C2D /* SourceControlPullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceControlPullView.swift; sourceTree = "<group>"; };
@@ -1248,6 +1251,7 @@
12481251
B6CFD8102C20A8EE00E63F1A /* NSFont+WithWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFont+WithWeight.swift"; sourceTree = "<group>"; };
12491252
B6D7EA582971078500301FAC /* InspectorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorSection.swift; sourceTree = "<group>"; };
12501253
B6D7EA5B297107DD00301FAC /* InspectorField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorField.swift; sourceTree = "<group>"; };
1254+
B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitConfigClient.swift; sourceTree = "<group>"; };
12511255
B6E41C6F29DD157F0088F9F4 /* AccountsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettingsView.swift; sourceTree = "<group>"; };
12521256
B6E41C7329DD40010088F9F4 /* View+HideSidebarToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HideSidebarToggle.swift"; sourceTree = "<group>"; };
12531257
B6E41C7829DE02800088F9F4 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
@@ -2252,6 +2256,7 @@
22522256
isa = PBXGroup;
22532257
children = (
22542258
58A5DF7F29325B5A00D1BD5D /* GitClient.swift */,
2259+
B6E38E012CD3E62E00F4E65A /* GitConfigClient.swift */,
22552260
04BA7C182AE2D7C600584E1C /* GitClient+Branches.swift */,
22562261
04BA7C1D2AE2D8A000584E1C /* GitClient+Clone.swift */,
22572262
04BA7C1B2AE2D84100584E1C /* GitClient+Commit.swift */,
@@ -2435,6 +2440,7 @@
24352440
5831E3C92933E83400D5A6D2 /* Protocols */,
24362441
5875680E29316BDC00C965A3 /* ShellClient */,
24372442
6C5C891A2A3F736500A94FE1 /* FocusedValues.swift */,
2443+
B67DBB872CD51C51007F4F18 /* Limiter.swift */,
24382444
);
24392445
path = Utils;
24402446
sourceTree = "<group>";
@@ -4025,6 +4031,7 @@
40254031
58798233292E30B90085B254 /* FeedbackToolbar.swift in Sources */,
40264032
6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */,
40274033
587B9E6829301D8F00AC7927 /* GitLabAccountModel.swift in Sources */,
4034+
B67DBB882CD51C55007F4F18 /* Limiter.swift in Sources */,
40284035
5878DAA7291AE76700DD95A3 /* OpenQuicklyViewModel.swift in Sources */,
40294036
6CFF967429BEBCC300182D6F /* FindCommands.swift in Sources */,
40304037
587B9E6529301D8F00AC7927 /* GitLabGroupAccess.swift in Sources */,
@@ -4241,6 +4248,7 @@
42414248
581550D429FBD37D00684881 /* ProjectNavigatorToolbarBottom.swift in Sources */,
42424249
66AF6CE72BF17FFB00D83C9D /* UpdateStatusBarInfo.swift in Sources */,
42434250
587B9E7E29301D8F00AC7927 /* GitHubGistRouter.swift in Sources */,
4251+
B6E38E022CD3E63A00F4E65A /* GitConfigClient.swift in Sources */,
42444252
B6AB09A52AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift in Sources */,
42454253
04BA7C0B2AE2A2D100584E1C /* GitBranch.swift in Sources */,
42464254
6CAAF69229BCC71C00A1F48A /* (null) in Sources */,

CodeEdit/Features/Settings/Pages/SourceControlSettings/Models/SourceControlSettings.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,31 +130,18 @@ extension SettingsData {
130130
}
131131

132132
struct SourceControlGit: Codable, Hashable {
133-
/// The author name
134-
var authorName: String = ""
135-
/// The author email
136-
var authorEmail: String = ""
137-
/// Indicates what files should be ignored when committing
138133
var ignoredFiles: [IgnoredFiles] = []
139134
/// Indicates whether we should rebase when pulling commits
140-
var preferRebaseWhenPulling: Bool = false
141-
/// Indicates whether we should show commits per file log
142135
var showMergeCommitsPerFileLog: Bool = false
143136
/// Default initializer
144137
init() {}
145138
/// Explicit decoder init for setting default values when key is not present in `JSON`
146139
init(from decoder: Decoder) throws {
147140
let container = try decoder.container(keyedBy: CodingKeys.self)
148-
self.authorName = try container.decodeIfPresent(String.self, forKey: .authorName) ?? ""
149-
self.authorEmail = try container.decodeIfPresent(String.self, forKey: .authorEmail) ?? ""
150141
self.ignoredFiles = try container.decodeIfPresent(
151142
[IgnoredFiles].self,
152143
forKey: .ignoredFiles
153144
) ?? []
154-
self.preferRebaseWhenPulling = try container.decodeIfPresent(
155-
Bool.self,
156-
forKey: .preferRebaseWhenPulling
157-
) ?? false
158145
self.showMergeCommitsPerFileLog = try container.decodeIfPresent(
159146
Bool.self,
160147
forKey: .showMergeCommitsPerFileLog

CodeEdit/Features/Settings/Pages/SourceControlSettings/SourceControlGitView.swift

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ struct SourceControlGitView: View {
1111
@AppSettings(\.sourceControl.git)
1212
var git
1313

14-
@State var ignoredFileSelection: IgnoredFiles.ID?
14+
let gitConfig = GitConfigClient(shellClient: currentWorld.shellClient)
15+
16+
@State private var authorName: String = ""
17+
@State private var authorEmail: String = ""
18+
@State private var preferRebaseWhenPulling: Bool = false
19+
@State private var hasAppeared = false
1520

1621
var body: some View {
1722
SettingsForm {
@@ -24,23 +29,61 @@ struct SourceControlGitView: View {
2429
showMergeCommitsInPerFileLog
2530
}
2631
}
32+
.onAppear {
33+
Task {
34+
authorName = try await gitConfig.get(key: "user.name", global: true) ?? ""
35+
authorEmail = try await gitConfig.get(key: "user.email", global: true) ?? ""
36+
preferRebaseWhenPulling = try await gitConfig.get(key: "pull.rebase", global: true) ?? false
37+
Task {
38+
hasAppeared = true
39+
}
40+
}
41+
}
2742
}
2843
}
2944

3045
private extension SourceControlGitView {
3146
private var gitAuthorName: some View {
32-
TextField("Author Name", text: $git.authorName)
47+
TextField("Author Name", text: $authorName)
48+
.onChange(of: authorName) { newValue in
49+
if hasAppeared {
50+
Limiter.debounce(id: "authorNameDebouncer", duration: 0.5) {
51+
Task {
52+
await gitConfig.set(key: "user.name", value: newValue, global: true)
53+
}
54+
}
55+
}
56+
}
3357
}
3458

3559
private var gitEmail: some View {
36-
TextField("Author Email", text: $git.authorEmail)
60+
TextField("Author Email", text: $authorEmail)
61+
.onChange(of: authorEmail) { newValue in
62+
if hasAppeared {
63+
Limiter.debounce(id: "authorEmailDebouncer", duration: 0.5) {
64+
Task {
65+
await gitConfig.set(key: "user.email", value: newValue, global: true)
66+
}
67+
}
68+
}
69+
}
3770
}
3871

3972
private var preferToRebaseWhenPulling: some View {
4073
Toggle(
4174
"Prefer to rebase when pulling",
42-
isOn: $git.preferRebaseWhenPulling
75+
isOn: $preferRebaseWhenPulling
4376
)
77+
.onChange(of: preferRebaseWhenPulling) { newValue in
78+
if hasAppeared {
79+
Limiter.debounce(id: "pullRebaseDebouncer", duration: 0.5) {
80+
Task {
81+
print("Setting pull.rebase to \(newValue)")
82+
await gitConfig.set(key: "pull.rebase", value: newValue, global: true)
83+
}
84+
}
85+
}
86+
}
4487
}
4588

4689
private var showMergeCommitsInPerFileLog: some View {

CodeEdit/Features/SourceControl/Client/GitClient+Pull.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ import Foundation
1010
extension GitClient {
1111
/// Pull changes from remote
1212
func pullFromRemote(remote: String? = nil, branch: String? = nil, rebase: Bool = false) async throws {
13-
var command = "pull"
13+
var command = "pull \(rebase ? "--rebase" : "--no-rebase")"
1414

1515
if let remote = remote, let branch = branch {
1616
command += " \(remote) \(branch)"
1717
}
1818

19-
if rebase {
20-
command += " --rebase"
21-
}
22-
2319
_ = try await self.run(command)
2420
}
2521
}

CodeEdit/Features/SourceControl/Client/GitClient.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,20 @@ class GitClient {
3838
internal let directoryURL: URL
3939
internal let shellClient: ShellClient
4040

41+
private let configClient: GitConfigClient
42+
4143
init(directoryURL: URL, shellClient: ShellClient) {
4244
self.directoryURL = directoryURL
4345
self.shellClient = shellClient
46+
self.configClient = GitConfigClient(projectURL: directoryURL, shellClient: shellClient)
47+
}
48+
49+
func getConfig<T: GitConfigRepresentable>(key: String) async throws -> T? {
50+
return try await configClient.get(key: key, global: false)
51+
}
52+
53+
func setConfig<T: GitConfigRepresentable>(key: String, value: T) async {
54+
await configClient.set(key: key, value: value, global: false)
4455
}
4556

4657
/// Runs a git command, it will prepend the command with `cd <directoryURL>;git`,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// GitConfigClient.swift
3+
// CodeEdit
4+
//
5+
// Created by Austin Condiff on 10/31/24.
6+
//
7+
8+
import Foundation
9+
10+
protocol GitConfigRepresentable {
11+
init?(configValue: String)
12+
var asConfigValue: String { get }
13+
}
14+
15+
extension Bool: GitConfigRepresentable {
16+
init?(configValue: String) {
17+
switch configValue.lowercased() {
18+
case "true": self = true
19+
case "false": self = false
20+
default: return nil
21+
}
22+
}
23+
24+
var asConfigValue: String {
25+
self ? "true" : "false"
26+
}
27+
}
28+
29+
extension String: GitConfigRepresentable {
30+
init?(configValue: String) {
31+
self = configValue
32+
}
33+
34+
var asConfigValue: String {
35+
"\"\(self)\""
36+
}
37+
}
38+
39+
class GitConfigClient {
40+
private let projectURL: URL?
41+
private let shellClient: ShellClient
42+
43+
init(projectURL: URL? = nil, shellClient: ShellClient) {
44+
self.projectURL = projectURL
45+
self.shellClient = shellClient
46+
}
47+
48+
private func runConfigCommand(_ command: String, global: Bool) async throws -> String {
49+
var fullCommand = "git config"
50+
51+
if global {
52+
fullCommand += " --global"
53+
} else if let projectURL = projectURL {
54+
fullCommand = "cd \(projectURL.relativePath.escapedWhiteSpaces()); " + fullCommand
55+
}
56+
57+
fullCommand += " \(command)"
58+
return try shellClient.run(fullCommand)
59+
}
60+
61+
func get<T: GitConfigRepresentable>(key: String, global: Bool = false) async throws -> T? {
62+
let output = try await runConfigCommand(key, global: global)
63+
let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
64+
return T(configValue: trimmedOutput)
65+
}
66+
67+
func set<T: GitConfigRepresentable>(key: String, value: T, global: Bool = false) async {
68+
let shouldUnset: Bool
69+
if let boolValue = value as? Bool {
70+
shouldUnset = !boolValue
71+
} else if let stringValue = value as? String {
72+
shouldUnset = stringValue.isEmpty
73+
} else {
74+
shouldUnset = false
75+
}
76+
77+
let commandString = shouldUnset ? "--unset \(key)" : "\(key) \(value.asConfigValue)"
78+
79+
do {
80+
_ = try await runConfigCommand(commandString, global: global)
81+
} catch {
82+
print("Failed to set \(key): \(error)")
83+
}
84+
}
85+
}

CodeEdit/Features/SourceControl/Views/SourceControlPullView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ struct SourceControlPullView: View {
1515

1616
@State var loading: Bool = false
1717

18+
@AppSettings(\.sourceControl.git.preferRebaseWhenPulling)
19+
var preferRebaseWhenPulling
20+
1821
var body: some View {
1922
VStack(spacing: 0) {
2023
Form {
@@ -35,6 +38,11 @@ struct SourceControlPullView: View {
3538
.formStyle(.grouped)
3639
.scrollDisabled(true)
3740
.scrollContentBackground(.hidden)
41+
.onAppear {
42+
if preferRebaseWhenPulling {
43+
sourceControlManager.operationRebase = true
44+
}
45+
}
3846
HStack {
3947
if loading {
4048
HStack(spacing: 7.5) {

CodeEdit/Utils/Limiter.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// Limiter.swift
3+
// CodeEdit
4+
//
5+
// Created by Austin Condiff on 11/1/24.
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
enum Limiter {
12+
// Keep track of debounce timers and throttle states
13+
private static var debounceTimers: [AnyHashable: AnyCancellable] = [:]
14+
private static var throttleLastExecution: [AnyHashable: Date] = [:]
15+
16+
/// Debounces an action with a specified duration and identifier.
17+
/// - Parameters:
18+
/// - id: A unique identifier for the debounced action.
19+
/// - duration: The debounce duration in seconds.
20+
/// - action: The action to be executed after the debounce period.
21+
static func debounce(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) {
22+
// Cancel any existing debounce timer for the given ID
23+
debounceTimers[id]?.cancel()
24+
25+
// Start a new debounce timer for the given ID
26+
debounceTimers[id] = Timer.publish(every: duration, on: .main, in: .common)
27+
.autoconnect()
28+
.first()
29+
.sink { _ in
30+
action()
31+
debounceTimers[id] = nil
32+
}
33+
}
34+
35+
/// Throttles an action with a specified duration and identifier.
36+
/// - Parameters:
37+
/// - id: A unique identifier for the throttled action.
38+
/// - duration: The throttle duration in seconds.
39+
/// - action: The action to be executed after the throttle period.
40+
static func throttle(id: AnyHashable, duration: TimeInterval, action: @escaping () -> Void) {
41+
// Check the time of the last execution for the given ID
42+
if let lastExecution = throttleLastExecution[id], Date().timeIntervalSince(lastExecution) < duration {
43+
return // Skip this call if it's within the throttle duration
44+
}
45+
46+
// Update the last execution time and perform the action
47+
throttleLastExecution[id] = Date()
48+
action()
49+
}
50+
}

0 commit comments

Comments
 (0)