Skip to content

Commit 09953da

Browse files
committed
Fix #304
Adds a "Restore File to Previous Version" option. We currently have a "Restore File to This Version", but it's common to want to get the file before the change in a known commit.
1 parent 18da35a commit 09953da

File tree

4 files changed

+148
-2
lines changed

4 files changed

+148
-2
lines changed

GitUpKit/Core/GCRepository+HEAD-Tests.m

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ - (void)testUnbornHEAD {
3636

3737
@end
3838

39-
// TODO: Test -checkoutFileToWorkingDirectory:fromCommit:skipIndex:error:
4039
@implementation GCMultipleCommitsRepositoryTests (GCRepository_HEAD)
4140

4241
// -checkoutIndex:withOptions:error: is tested in GCRepository+Bare
@@ -112,4 +111,87 @@ - (void)testHEAD {
112111
XCTAssertFalse([self.repository checkClean:0 error:NULL]);
113112
}
114113

114+
- (void)testCheckoutFileToWorkingDirectory {
115+
// Working directory should have content from commit3 (master)
116+
[self assertContentsOfFileAtPath:@"hello_world.txt" equalsString:@"Hola Mundo!\n"];
117+
118+
// Checkout file from commit1 (earlier version)
119+
XCTAssertTrue([self.repository checkoutFileToWorkingDirectory:@"hello_world.txt" fromCommit:self.commit1 skipIndex:YES error:NULL]);
120+
[self assertContentsOfFileAtPath:@"hello_world.txt" equalsString:@"Bonjour Monde!\n"];
121+
122+
// Checkout file from initialCommit
123+
XCTAssertTrue([self.repository checkoutFileToWorkingDirectory:@"hello_world.txt" fromCommit:self.initialCommit skipIndex:YES error:NULL]);
124+
[self assertContentsOfFileAtPath:@"hello_world.txt" equalsString:@"Hello World!\n"];
125+
126+
// Checkout file from commit2
127+
XCTAssertTrue([self.repository checkoutFileToWorkingDirectory:@"hello_world.txt" fromCommit:self.commit2 skipIndex:YES error:NULL]);
128+
[self assertContentsOfFileAtPath:@"hello_world.txt" equalsString:@"Gutentag Welt!\n"];
129+
}
130+
131+
- (void)testLookupParentsForCommit {
132+
// Test parent of commit1 is initialCommit
133+
NSArray* parentsOfCommit1 = [self.repository lookupParentsForCommit:self.commit1 error:NULL];
134+
XCTAssertEqual(parentsOfCommit1.count, 1);
135+
XCTAssertEqualObjects(parentsOfCommit1.firstObject, self.initialCommit);
136+
137+
// Test parent of commit2 is commit1
138+
NSArray* parentsOfCommit2 = [self.repository lookupParentsForCommit:self.commit2 error:NULL];
139+
XCTAssertEqual(parentsOfCommit2.count, 1);
140+
XCTAssertEqualObjects(parentsOfCommit2.firstObject, self.commit1);
141+
142+
// Test parent of commit3 is commit2
143+
NSArray* parentsOfCommit3 = [self.repository lookupParentsForCommit:self.commit3 error:NULL];
144+
XCTAssertEqual(parentsOfCommit3.count, 1);
145+
XCTAssertEqualObjects(parentsOfCommit3.firstObject, self.commit2);
146+
147+
// Test parent of commitA is initialCommit (branched from there)
148+
NSArray* parentsOfCommitA = [self.repository lookupParentsForCommit:self.commitA error:NULL];
149+
XCTAssertEqual(parentsOfCommitA.count, 1);
150+
XCTAssertEqualObjects(parentsOfCommitA.firstObject, self.initialCommit);
151+
152+
// Test parent of initialCommit is empty (root commit)
153+
NSArray* parentsOfInitial = [self.repository lookupParentsForCommit:self.initialCommit error:NULL];
154+
XCTAssertEqual(parentsOfInitial.count, 0);
155+
}
156+
157+
- (void)testCheckTreeForCommitContainsFile {
158+
// hello_world.txt should exist in all commits
159+
XCTAssertNotNil([self.repository checkTreeForCommit:self.initialCommit containsFile:@"hello_world.txt" error:NULL]);
160+
XCTAssertNotNil([self.repository checkTreeForCommit:self.commit1 containsFile:@"hello_world.txt" error:NULL]);
161+
XCTAssertNotNil([self.repository checkTreeForCommit:self.commit2 containsFile:@"hello_world.txt" error:NULL]);
162+
XCTAssertNotNil([self.repository checkTreeForCommit:self.commit3 containsFile:@"hello_world.txt" error:NULL]);
163+
XCTAssertNotNil([self.repository checkTreeForCommit:self.commitA containsFile:@"hello_world.txt" error:NULL]);
164+
165+
// A non-existent file should return nil
166+
XCTAssertNil([self.repository checkTreeForCommit:self.commit1 containsFile:@"nonexistent.txt" error:NULL]);
167+
}
168+
169+
- (void)testRestoreFileToParentVersion {
170+
// Create a new file in a new commit
171+
GCCommit* addCommit = [self makeCommitWithUpdatedFileAtPath:@"new_file.txt" string:@"New Content\n" message:@"Add new file"];
172+
XCTAssertNotNil(addCommit);
173+
[self assertContentsOfFileAtPath:@"new_file.txt" equalsString:@"New Content\n"];
174+
175+
// Verify the file exists in addCommit
176+
XCTAssertNotNil([self.repository checkTreeForCommit:addCommit containsFile:@"new_file.txt" error:NULL]);
177+
178+
// Verify the file doesn't exist in commit3 (before addCommit)
179+
XCTAssertNil([self.repository checkTreeForCommit:self.commit3 containsFile:@"new_file.txt" error:NULL]);
180+
181+
// Make another commit modifying the file
182+
GCCommit* modifyCommit = [self makeCommitWithUpdatedFileAtPath:@"new_file.txt" string:@"Modified Content\n" message:@"Modify file"];
183+
XCTAssertNotNil(modifyCommit);
184+
[self assertContentsOfFileAtPath:@"new_file.txt" equalsString:@"Modified Content\n"];
185+
186+
// Restore file to version before modifyCommit (should get addCommit's version)
187+
NSArray* parentsOfModify = [self.repository lookupParentsForCommit:modifyCommit error:NULL];
188+
GCCommit* parentCommit = parentsOfModify.firstObject;
189+
XCTAssertNotNil(parentCommit);
190+
XCTAssertEqualObjects(parentCommit, addCommit);
191+
192+
// Checkout file from parent commit
193+
XCTAssertTrue([self.repository checkoutFileToWorkingDirectory:@"new_file.txt" fromCommit:parentCommit skipIndex:YES error:NULL]);
194+
[self assertContentsOfFileAtPath:@"new_file.txt" equalsString:@"New Content\n"];
195+
}
196+
115197
@end

GitUpKit/Utilities/GIViewController+Utilities.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
- (void)deleteUntrackedFile:(NSString*)path; // Prompts user
4747

4848
- (void)restoreFile:(NSString*)path toCommit:(GCCommit*)commit; // Prompts user
49+
- (void)restoreFile:(NSString*)path toBeforeCommit:(GCCommit*)commit; // Prompts user
4950

5051
- (void)showFileInFinder:(NSString*)path;
5152
- (void)openFileWithDefaultEditor:(NSString*)path;

GitUpKit/Utilities/GIViewController+Utilities.m

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ - (void)deleteUntrackedFile:(NSString*)path {
290290
- (void)restoreFile:(NSString*)path toCommit:(GCCommit*)commit {
291291
[self confirmUserActionWithAlertType:kGIAlertType_Stop
292292
title:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to restore the file \"%@\" to the version from this commit?", nil), path.lastPathComponent]
293-
message:NSLocalizedString(@"Any local changes will be overwritten. This action cannot be undone.", nil)
293+
message:NSLocalizedString(@"Any local changes to this file will be overwritten. This action cannot be undone.", nil)
294294
button:NSLocalizedString(@"Restore", nil)
295295
suppressionUserDefaultKey:nil
296296
block:^{
@@ -302,6 +302,60 @@ - (void)restoreFile:(NSString*)path toCommit:(GCCommit*)commit {
302302
}];
303303
}
304304

305+
- (void)restoreFile:(NSString*)path toBeforeCommit:(GCCommit*)commit {
306+
NSError* lookupError;
307+
NSArray* parents = [self.repository lookupParentsForCommit:commit error:&lookupError];
308+
GCCommit* parentCommit = parents.firstObject;
309+
310+
if (parentCommit) {
311+
// Check if the file existed in the parent commit
312+
NSString* sha1 = [self.repository checkTreeForCommit:parentCommit containsFile:path error:NULL];
313+
if (sha1) {
314+
// File existed in parent - restore to that version
315+
[self confirmUserActionWithAlertType:kGIAlertType_Stop
316+
title:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to restore the file \"%@\" to the version before this commit?", nil), path.lastPathComponent]
317+
message:NSLocalizedString(@"Any local changes to this file will be overwritten. This action cannot be undone.", nil)
318+
button:NSLocalizedString(@"Restore", nil)
319+
suppressionUserDefaultKey:nil
320+
block:^{
321+
NSError* error;
322+
if (![self.repository safeDeleteFileIfExists:path error:&error] || ![self.repository checkoutFileToWorkingDirectory:path fromCommit:parentCommit skipIndex:YES error:&error]) {
323+
[self presentError:error];
324+
}
325+
[self.repository notifyWorkingDirectoryChanged];
326+
}];
327+
} else {
328+
// File didn't exist in parent - it was added in this commit, so delete it
329+
[self confirmUserActionWithAlertType:kGIAlertType_Stop
330+
title:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the file \"%@\"?", nil), path.lastPathComponent]
331+
message:NSLocalizedString(@"The file did not exist before this commit. Any local changes to this file will be lost. This action cannot be undone.", nil)
332+
button:NSLocalizedString(@"Delete", nil)
333+
suppressionUserDefaultKey:nil
334+
block:^{
335+
NSError* error;
336+
if (![self.repository safeDeleteFileIfExists:path error:&error]) {
337+
[self presentError:error];
338+
}
339+
[self.repository notifyWorkingDirectoryChanged];
340+
}];
341+
}
342+
} else {
343+
// No parent commit (root commit) - file was added in this commit, so delete it
344+
[self confirmUserActionWithAlertType:kGIAlertType_Stop
345+
title:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to delete the file \"%@\"?", nil), path.lastPathComponent]
346+
message:NSLocalizedString(@"The file did not exist before this commit. Any local changes to this file will be lost. This action cannot be undone.", nil)
347+
button:NSLocalizedString(@"Delete", nil)
348+
suppressionUserDefaultKey:nil
349+
block:^{
350+
NSError* error;
351+
if (![self.repository safeDeleteFileIfExists:path error:&error]) {
352+
[self presentError:error];
353+
}
354+
[self.repository notifyWorkingDirectoryChanged];
355+
}];
356+
}
357+
}
358+
305359
- (void)openFileWithDefaultEditor:(NSString*)path {
306360
[[NSWorkspace sharedWorkspace] openURL:[self.repository absoluteURLForFile:path]]; // This will silently fail if the file doesn't exist in the working directory
307361
}

GitUpKit/Views/GIQuickViewController.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ - (NSMenu*)diffContentsViewController:(GIDiffContentsViewController*)controller
219219
[menu addItemWithTitle:NSLocalizedString(@"Restore File to This Version…", nil) block:NULL];
220220
}
221221

222+
if (GC_FILE_MODE_IS_FILE(delta.oldFile.mode) || GC_FILE_MODE_IS_FILE(delta.newFile.mode)) {
223+
[menu addItemWithTitle:NSLocalizedString(@"Restore File to Previous Version…", nil)
224+
block:^{
225+
[self restoreFile:delta.canonicalPath toBeforeCommit:_commit];
226+
}];
227+
} else {
228+
[menu addItemWithTitle:NSLocalizedString(@"Restore File to Previous Version…", nil) block:NULL];
229+
}
230+
222231
return menu;
223232
}
224233

0 commit comments

Comments
 (0)