Skip to content

Commit 3f5abaf

Browse files
lapfelixlucasderraugh
authored andcommitted
Faster multiple file staging (#629)
* Improve performance when staging/unstaging multiple files at once * Optimize multiple files discarding * Run ClangFormat * Changes following PR feedback * Fix bug caused by bad merge and add comments The selectedFiles array would always be empty because the `else` should've been outside the `if (submodule)` * Correctly allocate and free multiple path strings
1 parent 2e9b935 commit 3f5abaf

File tree

9 files changed

+213
-41
lines changed

9 files changed

+213
-41
lines changed

GitUpKit/Core/GCDiff-Tests.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ - (void)testUnifiedDiff {
165165
XCTAssertTrue([self.repository addFileToIndex:@"renamed1.txt" error:NULL]);
166166
[self updateFileAtPath:@"type-changed.txt" withString:@""];
167167
XCTAssertTrue([self.repository addFileToIndex:@"type-changed.txt" error:NULL]);
168+
169+
NSArray* files = @[ @".gitignore", @"modified.txt", @"deleted.txt", @"renamed1.txt", @"type-changed.txt" ];
170+
171+
// Test adding and removing multiple files
172+
XCTAssertTrue([self.repository removeFilesFromIndex:files error:NULL]);
173+
XCTAssertEqual([self.repository checkIndexStatus:NULL].deltas.count, 0);
174+
175+
XCTAssertTrue([self.repository addFilesToIndex:files error:NULL]);
176+
XCTAssertEqual([self.repository checkIndexStatus:NULL].deltas.count, 5);
177+
168178
XCTAssertNotNil([self.repository createCommitFromHEADWithMessage:@"Update" error:NULL]);
169179

170180
// Touch files
@@ -179,8 +189,8 @@ - (void)testUnifiedDiff {
179189

180190
// Stage some files
181191
XCTAssertTrue([self.repository addFileToIndex:@"modified.txt" error:NULL]);
182-
XCTAssertTrue([self.repository removeFileFromIndex:@"deleted.txt" error:NULL]);
183-
XCTAssertTrue([self.repository removeFileFromIndex:@"renamed1.txt" error:NULL]);
192+
files = @[ @"deleted.txt", @"renamed1.txt" ];
193+
XCTAssertTrue([self.repository removeFilesFromIndex:files error:NULL]);
184194
XCTAssertTrue([self.repository addFileToIndex:@"renamed2.txt" error:NULL]);
185195
XCTAssertTrue([self.repository addFileToIndex:@"added.txt" error:NULL]);
186196

GitUpKit/Core/GCIndex.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ typedef BOOL (^GCIndexLineFilter)(GCLineDiffChange change, NSUInteger oldLineNum
6666
- (BOOL)resetLinesInFile:(NSString*)path index:(GCIndex*)index toCommit:(GCCommit*)commit error:(NSError**)error usingFilter:(GCIndexLineFilter)filter;
6767

6868
- (BOOL)checkoutFileToWorkingDirectory:(NSString*)path fromIndex:(GCIndex*)index error:(NSError**)error;
69+
- (BOOL)checkoutFilesToWorkingDirectory:(NSArray<NSString*>*)paths fromIndex:(GCIndex*)index error:(NSError**)error;
6970
- (BOOL)checkoutLinesInFileToWorkingDirectory:(NSString*)path fromIndex:(GCIndex*)index error:(NSError**)error usingFilter:(GCIndexLineFilter)filter;
7071

7172
- (BOOL)clearConflictForFile:(NSString*)path inIndex:(GCIndex*)index error:(NSError**)error;

GitUpKit/Core/GCIndex.m

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,12 +485,24 @@ - (BOOL)resetLinesInFile:(NSString*)path index:(GCIndex*)index toCommit:(GCCommi
485485
}
486486

487487
- (BOOL)checkoutFileToWorkingDirectory:(NSString*)path fromIndex:(GCIndex*)index error:(NSError**)error {
488+
return [self checkoutFilesToWorkingDirectory:@[ path ]
489+
fromIndex:index
490+
error:error];
491+
}
492+
493+
- (BOOL)checkoutFilesToWorkingDirectory:(NSArray<NSString*>*)paths fromIndex:(GCIndex*)index error:(NSError**)error {
488494
git_checkout_options options = GIT_CHECKOUT_OPTIONS_INIT;
489495
options.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_DONT_UPDATE_INDEX | GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH; // There's no reason to update the index
490-
options.paths.count = 1;
491-
const char* filePath = GCGitPathFromFileSystemPath(path);
492-
options.paths.strings = (char**)&filePath;
496+
options.paths.count = paths.count;
497+
char** pathStrings = malloc(paths.count * sizeof(char*));
498+
options.paths.strings = pathStrings;
499+
for (NSUInteger i = 0; i < paths.count; i++) {
500+
const char* filePath = GCGitPathFromFileSystemPath(paths[i]);
501+
options.paths.strings[i] = (char*)filePath;
502+
}
503+
493504
CALL_LIBGIT2_FUNCTION_RETURN(NO, git_checkout_index, self.private, index.private, &options);
505+
free(pathStrings);
494506
return YES;
495507
}
496508

GitUpKit/Extensions/GCRepository+Index-Tests.m

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,33 @@ - (void)testIndex {
6969
XCTAssertTrue([self.repository addFileToIndex:@"test.txt" error:NULL]);
7070
[self assertGitCLTOutputEqualsString:@"A test.txt\n" withRepository:self.repository command:@"status", @"--ignored", @"--porcelain", nil];
7171

72+
// Add multiple files to working directory
73+
NSMutableArray* filePaths = [[NSMutableArray alloc] init];
74+
NSString* expectedGitCLTOutput = [[NSString alloc] init];
75+
for (int i = 0; i < 50; i++) {
76+
NSString* filePath = [NSString stringWithFormat:@"hello_world%02d.txt", i];
77+
[self updateFileAtPath:filePath withString:@"Bonjour le monde!\n"];
78+
[filePaths addObject:filePath];
79+
expectedGitCLTOutput = [expectedGitCLTOutput stringByAppendingFormat:@"A %@\n", filePath];
80+
}
81+
expectedGitCLTOutput = [expectedGitCLTOutput stringByAppendingString:@"A test.txt\n"];
82+
83+
// Add multiple files to index
84+
XCTAssertTrue([self.repository addFilesToIndex:filePaths error:NULL]);
85+
[self assertGitCLTOutputEqualsString:expectedGitCLTOutput withRepository:self.repository command:@"status", @"--ignored", @"--porcelain", nil];
86+
87+
// Add remove multiple files from index
88+
XCTAssertTrue([self.repository removeFilesFromIndex:filePaths error:NULL]);
89+
expectedGitCLTOutput = [expectedGitCLTOutput stringByReplacingOccurrencesOfString:@"A test.txt\n" withString:@""];
90+
expectedGitCLTOutput = [expectedGitCLTOutput stringByReplacingOccurrencesOfString:@"A hello_world" withString:@"?? hello_world"];
91+
expectedGitCLTOutput = [@"A test.txt\n" stringByAppendingString:expectedGitCLTOutput];
92+
[self assertGitCLTOutputEqualsString:expectedGitCLTOutput withRepository:self.repository command:@"status", @"--ignored", @"--porcelain", nil];
93+
7294
// Reset index
7395
XCTAssertTrue([self.repository resetIndexToHEAD:NULL]);
74-
[self assertGitCLTOutputEqualsString:@"?? test.txt\n" withRepository:self.repository command:@"status", @"--ignored", @"--porcelain", nil];
96+
expectedGitCLTOutput = [expectedGitCLTOutput stringByReplacingOccurrencesOfString:@"A test.txt\n" withString:@""];
97+
expectedGitCLTOutput = [expectedGitCLTOutput stringByAppendingString:@"?? test.txt\n"];
98+
[self assertGitCLTOutputEqualsString:expectedGitCLTOutput withRepository:self.repository command:@"status", @"--ignored", @"--porcelain", nil];
7599
}
76100

77101
- (void)testIndex_Lines {

GitUpKit/Extensions/GCRepository+Index.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
- (BOOL)resetIndexToHEAD:(NSError**)error; // Like git reset --mixed HEAD but does not update reflog
2020

2121
- (BOOL)removeFileFromIndex:(NSString*)path error:(NSError**)error; // git rm --cached {file} - Delete file from index
22+
- (BOOL)removeFilesFromIndex:(NSArray<NSString*>*)paths error:(NSError**)error; // git rm --cached {file} - Delete files from index
2223

2324
- (BOOL)addFileToIndex:(NSString*)path error:(NSError**)error; // git add {file} - Copy file from workdir to index (aka stage file)
25+
- (BOOL)addFilesToIndex:(NSArray<NSString*>*)paths error:(NSError**)error;
2426
- (BOOL)resetFileInIndexToHEAD:(NSString*)path error:(NSError**)error; // git reset --mixed {file} - Copy file from HEAD to index (aka unstage file)
27+
- (BOOL)resetFilesInIndexToHEAD:(NSArray<NSString*>*)paths error:(NSError**)error;
2528
- (BOOL)checkoutFileFromIndex:(NSString*)path error:(NSError**)error; // git checkout {file} - Copy file from index to workdir (aka discard file)
29+
- (BOOL)checkoutFilesFromIndex:(NSArray<NSString*>*)paths error:(NSError**)error;
2630

2731
- (BOOL)addLinesFromFileToIndex:(NSString*)path error:(NSError**)error usingFilter:(GCIndexLineFilter)filter; // git add -p {file} - Copy only some lines of file from workdir to index (aka stage lines)
2832
- (BOOL)resetLinesFromFileInIndexToHEAD:(NSString*)path error:(NSError**)error usingFilter:(GCIndexLineFilter)filter; // git reset -p {file} - Copy only some lines of file from HEAD to index (aka unstage lines)

GitUpKit/Extensions/GCRepository+Index.m

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,61 @@ - (BOOL)resetIndexToHEAD:(NSError**)error {
3939
}
4040

4141
- (BOOL)removeFileFromIndex:(NSString*)path error:(NSError**)error {
42+
return [self removeFilesFromIndex:@[ path ] error:error];
43+
}
44+
45+
- (BOOL)removeFilesFromIndex:(NSArray<NSString*>*)paths error:(NSError**)error {
4246
GCIndex* index = [self readRepositoryIndex:error];
4347
if (index == nil) {
4448
return NO;
4549
}
46-
return [self removeFile:path fromIndex:index error:error] && [self writeRepositoryIndex:index error:error];
50+
51+
for (NSString* path in paths) {
52+
if (![self removeFile:path fromIndex:index error:error] || (error && *error != nil)) {
53+
[self writeRepositoryIndex:index error:error];
54+
return NO;
55+
}
56+
}
57+
58+
return [self writeRepositoryIndex:index error:error];
4759
}
4860

4961
- (BOOL)addFileToIndex:(NSString*)path error:(NSError**)error {
62+
return [self addFilesToIndex:@[ path ] error:error];
63+
}
64+
65+
- (BOOL)addFilesToIndex:(NSArray<NSString*>*)paths error:(NSError**)error {
5066
GCIndex* index = [self readRepositoryIndex:error];
5167
if (index == nil) {
5268
return NO;
5369
}
54-
return [self addFileInWorkingDirectory:path toIndex:index error:error] && [self writeRepositoryIndex:index error:error];
70+
71+
BOOL failed = NO;
72+
BOOL shouldWriteRepository = NO;
73+
for (NSString* path in paths) {
74+
if (![self addFileInWorkingDirectory:path toIndex:index error:error] || (error && *error != nil)) {
75+
failed = YES;
76+
break;
77+
}
78+
79+
shouldWriteRepository = YES;
80+
}
81+
82+
if (failed && shouldWriteRepository) {
83+
if (shouldWriteRepository) {
84+
[self writeRepositoryIndex:index error:NULL];
85+
}
86+
return NO;
87+
}
88+
89+
return [self writeRepositoryIndex:index error:error];
5590
}
5691

5792
- (BOOL)resetFileInIndexToHEAD:(NSString*)path error:(NSError**)error {
93+
return [self resetFilesInIndexToHEAD:@[ path ] error:error];
94+
}
95+
96+
- (BOOL)resetFilesInIndexToHEAD:(NSArray<NSString*>*)paths error:(NSError**)error {
5897
GCIndex* index = [self readRepositoryIndex:error];
5998
if (index == nil) {
6099
return NO;
@@ -63,24 +102,34 @@ - (BOOL)resetFileInIndexToHEAD:(NSString*)path error:(NSError**)error {
63102
if (![self lookupHEADCurrentCommit:&headCommit branch:NULL error:error]) {
64103
return NO;
65104
}
66-
if (headCommit) {
67-
if (![self resetFile:path inIndex:index toCommit:headCommit error:error]) {
68-
return NO;
69-
}
70-
} else {
71-
if (![self removeFile:path fromIndex:index error:error]) {
72-
return NO;
105+
106+
for (NSString* path in paths) {
107+
if (headCommit) {
108+
if (![self resetFile:path inIndex:index toCommit:headCommit error:error]) {
109+
[self writeRepositoryIndex:index error:error];
110+
return NO;
111+
}
112+
} else {
113+
if (![self removeFile:path fromIndex:index error:error]) {
114+
[self writeRepositoryIndex:index error:error];
115+
return NO;
116+
}
73117
}
74118
}
119+
75120
return [self writeRepositoryIndex:index error:error];
76121
}
77122

78123
- (BOOL)checkoutFileFromIndex:(NSString*)path error:(NSError**)error {
124+
return [self checkoutFilesFromIndex:@[ path ] error:error];
125+
}
126+
127+
- (BOOL)checkoutFilesFromIndex:(NSArray<NSString*>*)paths error:(NSError**)error {
79128
GCIndex* index = [self readRepositoryIndex:error];
80129
if (index == nil) {
81130
return NO;
82131
}
83-
return [self checkoutFileToWorkingDirectory:path fromIndex:index error:error];
132+
return [self checkoutFilesToWorkingDirectory:paths fromIndex:index error:error];
84133
}
85134

86135
- (BOOL)addLinesFromFileToIndex:(NSString*)path error:(NSError**)error usingFilter:(GCIndexLineFilter)filter {

GitUpKit/Utilities/GIViewController+Utilities.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ extern NSString* const GIViewController_TerminalTool_iTerm;
4848
- (void)discardSubmoduleAtPath:(NSString*)path resetIndex:(BOOL)resetIndex; // Prompts user
4949

5050
- (void)stageAllChangesForFile:(NSString*)path;
51+
- (void)stageAllChangesForFiles:(NSArray<NSString*>*)paths;
5152
- (void)stageSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLines newLines:(NSIndexSet*)newLines;
5253
- (void)unstageAllChangesForFile:(NSString*)path;
54+
- (void)unstageAllChangesForFiles:(NSArray<NSString*>*)paths;
5355
- (void)unstageSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLines newLines:(NSIndexSet*)newLines;
5456

5557
- (BOOL)discardAllChangesForFile:(NSString*)path resetIndex:(BOOL)resetIndex error:(NSError**)error;
58+
- (BOOL)discardAllChangesForFiles:(NSArray<NSString*>*)paths resetIndex:(BOOL)resetIndex error:(NSError**)error;
5659
- (void)discardAllChangesForFile:(NSString*)path resetIndex:(BOOL)resetIndex; // Prompts user
5760
- (BOOL)discardSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLines newLines:(NSIndexSet*)newLines resetIndex:(BOOL)resetIndex error:(NSError**)error;
5861
- (void)discardSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLines newLines:(NSIndexSet*)newLines resetIndex:(BOOL)resetIndex; // Prompts user

GitUpKit/Utilities/GIViewController+Utilities.m

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ + (void)initialize {
111111
GIViewController_TerminalTool : GIViewController_TerminalTool_Terminal,
112112
};
113113
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
114-
114+
115115
NSDictionary* installedApps = [GILaunchServicesLocator installedAppsDictionary];
116116
[[NSUserDefaults standardUserDefaults] registerDefaults:installedApps];
117117

@@ -198,13 +198,34 @@ - (void)discardSubmoduleAtPath:(NSString*)path resetIndex:(BOOL)resetIndex {
198198
}
199199

200200
- (void)stageAllChangesForFile:(NSString*)path {
201+
return [self stageAllChangesForFiles:@[ path ]];
202+
}
203+
204+
- (void)stageAllChangesForFiles:(NSArray<NSString*>*)paths {
201205
NSError* error;
202-
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:[self.repository absolutePathForFile:path] followLastSymlink:NO];
203-
if ((fileExists && [self.repository addFileToIndex:path error:&error]) || (!fileExists && [self.repository removeFileFromIndex:path error:&error])) {
204-
[self.repository notifyRepositoryChanged];
205-
} else {
206-
[self presentError:error];
206+
NSMutableArray* existingFiles = [NSMutableArray array];
207+
NSMutableArray* nonExistingFiles = [NSMutableArray array];
208+
for (NSString* path in paths) {
209+
if ([[NSFileManager defaultManager] fileExistsAtPath:[self.repository absolutePathForFile:path]]) {
210+
[existingFiles addObject:path];
211+
} else {
212+
[nonExistingFiles addObject:path];
213+
}
214+
}
215+
216+
if (existingFiles.count > 0) {
217+
if (![self.repository addFilesToIndex:existingFiles error:&error]) {
218+
[self presentError:error];
219+
}
220+
}
221+
222+
if (nonExistingFiles.count > 0) {
223+
if (![self.repository removeFilesFromIndex:nonExistingFiles error:&error]) {
224+
[self presentError:error];
225+
}
207226
}
227+
228+
[self.repository notifyRepositoryChanged];
208229
}
209230

210231
- (void)stageSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLines newLines:(NSIndexSet*)newLines {
@@ -227,8 +248,12 @@ - (void)stageSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldLin
227248
}
228249

229250
- (void)unstageAllChangesForFile:(NSString*)path {
251+
[self unstageAllChangesForFiles:@[ path ]];
252+
}
253+
254+
- (void)unstageAllChangesForFiles:(NSArray<NSString*>*)paths {
230255
NSError* error;
231-
if ([self.repository resetFileInIndexToHEAD:path error:&error]) {
256+
if ([self.repository resetFilesInIndexToHEAD:paths error:&error]) {
232257
[self.repository notifyWorkingDirectoryChanged];
233258
} else {
234259
[self presentError:error];
@@ -255,18 +280,36 @@ - (void)unstageSelectedChangesForFile:(NSString*)path oldLines:(NSIndexSet*)oldL
255280
}
256281

257282
- (BOOL)discardAllChangesForFile:(NSString*)path resetIndex:(BOOL)resetIndex error:(NSError**)error {
283+
return [self discardAllChangesForFiles:@[ path ]
284+
resetIndex:resetIndex
285+
error:error];
286+
}
287+
288+
- (BOOL)discardAllChangesForFiles:(NSArray<NSString*>*)paths resetIndex:(BOOL)resetIndex error:(NSError**)error {
258289
BOOL success = NO;
259290
if (resetIndex) {
260291
GCCommit* commit;
261-
if ([self.repository lookupHEADCurrentCommit:&commit branch:NULL error:error] && [self.repository resetFileInIndexToHEAD:path error:error]) {
262-
if (commit && [self.repository checkTreeForCommit:commit containsFile:path error:NULL]) {
263-
success = [self.repository safeDeleteFileIfExists:path error:error] && [self.repository checkoutFileFromIndex:path error:error];
264-
} else {
265-
success = [self.repository safeDeleteFile:path error:error];
292+
if ([self.repository lookupHEADCurrentCommit:&commit branch:NULL error:error] && [self.repository resetFilesInIndexToHEAD:paths error:error]) {
293+
success = YES;
294+
for (NSString* path in paths) {
295+
if (commit && [self.repository checkTreeForCommit:commit containsFile:path error:NULL]) {
296+
if (![self.repository safeDeleteFileIfExists:path error:error] && [self.repository checkoutFileFromIndex:path error:error]) {
297+
return NO;
298+
}
299+
} else {
300+
if (![self.repository safeDeleteFileIfExists:path error:error]) {
301+
return NO;
302+
}
303+
}
266304
}
267305
}
268306
} else {
269-
success = [self.repository safeDeleteFileIfExists:path error:error] && [self.repository checkoutFileFromIndex:path error:error];
307+
for (NSString* path in paths) {
308+
if (![self.repository safeDeleteFileIfExists:path error:error]) {
309+
return NO;
310+
}
311+
}
312+
success = [self.repository checkoutFilesFromIndex:paths error:error];
270313
}
271314
return success;
272315
}
@@ -922,7 +965,7 @@ - (void)launchDiffToolWithCommit:(GCCommit*)commit otherCommit:(GCCommit*)otherC
922965
} else if ([identifier isEqualToString:GIViewControllerTool_BeyondCompare]) {
923966
[self _runBeyondCompareWithArguments:@[ [NSString stringWithFormat:@"-title1=%@", oldTitle], [NSString stringWithFormat:@"-title2=%@", newTitle], oldPath, newPath ]];
924967
} else if ([identifier isEqualToString:GIViewControllerTool_P4Merge] || [identifier isEqualToString:GIViewControllerTool_GitTool]) {
925-
; // Handled above
968+
// Handled above
926969
} else if ([identifier isEqualToString:GIViewControllerTool_DiffMerge]) {
927970
[self _runDiffMergeToolWithArguments:@[ [NSString stringWithFormat:@"-t1=%@", oldTitle], [NSString stringWithFormat:@"-t2=%@", newTitle], oldPath, newPath ]];
928971
} else {

0 commit comments

Comments
 (0)