-
Notifications
You must be signed in to change notification settings - Fork 136
/
gitlet.js
executable file
·1997 lines (1668 loc) · 73.2 KB
/
gitlet.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env node
// [Home](/) | [GitHub](https://github.com/maryrosecook/gitlet)
// Preparation
// ------------
// I wrote Gitlet to show how Git works under the covers. I wrote
// it to be readable. I commented the code heavily.
// If you are not familiar with the basic Git commands, you can read
// Git in six hundred words (below).
// For a six thousand word deep dive into the innards of Git, you can
// read [Git from the inside
// out](http://maryrosecook.com/blog/post/git-from-the-inside-out).
// Git in six hundred words
// ------------------------
// Imagine you have a directory called `alpha`. It contains a file
// called `number.txt` that contains the text `first`.
// You run `git init` to set up `alpha` as a Git repository.
// You run `git add number.txt` to add `number.txt` to the index. The
// index is a list of all the files that Git is keeping track of. It
// maps filenames to the content of the files. It now has the mapping
// `number.txt -> first`. Running the add command has also added a
// blob object containing `first` to the Git object database.
// You run `git commit -m first`. This does three things. First, it
// creates a tree object in the objects database. This object
// represents the list of items in the top level of the alpha
// directory. This object has a pointer to the `first` blob object
// that was created when you ran `git add`. Second, it creates a
// commit object that represents the version of the repository that
// you have just committed. This includes a pointer to the tree
// object. Third, it points the master branch at the new commit
// object.
// You run `git clone . ../beta`. This creates a new directory called
// `beta`. It initializes it as a Git repository. It copies the
// objects in the alpha objects database to the beta objects
// database. It points the master branch on beta at the commit object
// that the master branch points at on the alpha repository. It sets
// the index on beta to mirror the content of the first commit. It
// updates your files - `number.txt` - to mirror the index.
// You move to the beta repository. You change the content of
// `number.txt` to `second`. You run `git add number.txt` and `git
// commit -m second`. The commit object that is created has a pointer
// to its parent, the first commit. The commit command points the
// master branch at the second commit.
// You move back to the alpha repository. You run `git remote add beta
// ../beta`. This sets the beta repository as a remote repository.
// You run `git pull beta master`.
// Under the covers, this runs `git fetch beta master`. This finds the
// objects for the second commit and copies them from the beta
// repository to the alpha repository. It points alpha's record of
// beta's master at the second commit object. It updates `FETCH_HEAD`
// to show that the master branch was fetched from the beta
// repository.
// Under the covers, the pull command runs `git merge
// FETCH_HEAD`. This reads `FETCH_HEAD`, which shows that the master
// branch on the beta repository was the most recently fetched
// branch. It gets the commit object that alpha's record of beta's
// master is pointing at. This is the second commit. The master branch
// on alpha is pointing at the first commit, which is the ancestor of
// the second commit. This means that, to complete the merge, the
// merge command can just point the master branch at the second
// commit. The merge command updates the index to mirror the contents
// of the second commit. It updates the working copy to mirror the
// index.
// You run `git branch red`. This creates a branch called `red` that
// points at the second commit object.
// You run `git checkout red`. Before the checkout, `HEAD` pointed at
// the master branch. It now points at the red branch. This makes the
// red branch the current branch.
// You set the content of `number.txt` to `third`, run `git add
// numbers.txt` and run `git commit -m third`.
// You run `git push beta red`. This finds the objects for the third
// commit and copies them from the alpha repository to the beta
// repository. It points the red branch on the beta repository at the
// third commit object, and that's it.
// Imports
// -------
var fs = require("fs");
var nodePath = require("path");
// Main Git API functions
// ----------------------
var gitlet = module.exports = {
// **init()** initializes the current directory as a new repository.
init: function(opts) {
// Abort if already a repository.
if (files.inRepo()) { return; }
opts = opts || {};
// Create a JS object that mirrors the Git basic directory
// structure.
var gitletStructure = {
HEAD: "ref: refs/heads/master\n",
// If `--bare` was passed, write to the Git config indicating
// that the repository is bare. If `--bare` was not passed,
// write to the Git config saying the repository is not bare.
config: config.objToStr({ core: { "": { bare: opts.bare === true }}}),
objects: {},
refs: {
heads: {},
}
};
// Write the standard Git directory structure using the
// `gitletStructure` JS object. If the repository is not bare,
// put the directories inside the `.gitlet` directory. If the
// repository is bare, put them in the top level of the
// repository.
files.writeFilesFromTree(opts.bare ? gitletStructure : { ".gitlet": gitletStructure },
process.cwd());
},
// **add()** adds files that match `path` to the index.
add: function(path, _) {
files.assertInRepo();
config.assertNotBare();
// Get the paths of all the files matching `path`.
var addedFiles = files.lsRecursive(path);
// Abort if no files matched `path`.
if (addedFiles.length === 0) {
throw new Error(files.pathFromRepoRoot(path) + " did not match any files");
// Otherwise, use the `update_index()` Git command to actually add
// the files.
} else {
addedFiles.forEach(function(p) { gitlet.update_index(p, { add: true }); });
}
},
// **rm()** removes files that match `path` from the index.
rm: function(path, opts) {
files.assertInRepo();
config.assertNotBare();
opts = opts || {};
// Get the paths of all files in the index that match `path`.
var filesToRm = index.matchingFiles(path);
// Abort if `-f` was passed. The removal of files with changes is
// not supported.
if (opts.f) {
throw new Error("unsupported");
// Abort if no files matched `path`.
} else if (filesToRm.length === 0) {
throw new Error(files.pathFromRepoRoot(path) + " did not match any files");
// Abort if `path` is a directory and `-r` was not passed.
} else if (fs.existsSync(path) && fs.statSync(path).isDirectory() && !opts.r) {
throw new Error("not removing " + path + " recursively without -r");
} else {
// Get a list of all files that are to be removed and have also
// been changed on disk. If this list is not empty then abort.
var changesToRm = util.intersection(diff.addedOrModifiedFiles(), filesToRm);
if (changesToRm.length > 0) {
throw new Error("these files have changes:\n" + changesToRm.join("\n") + "\n");
// Otherwise, remove the files that match `path`. Delete them
// from disk and remove from the index.
} else {
filesToRm.map(files.workingCopyPath).filter(fs.existsSync).forEach(fs.unlinkSync);
filesToRm.forEach(function(p) { gitlet.update_index(p, { remove: true }); });
}
}
},
// **commit()** creates a commit object that represents the current
// state of the index, writes the commit to the `objects` directory
// and points `HEAD` at the commit.
commit: function(opts) {
files.assertInRepo();
config.assertNotBare();
// Write a tree set of tree objects that represent the current
// state of the index.
var treeHash = gitlet.write_tree();
var headDesc = refs.isHeadDetached() ? "detached HEAD" : refs.headBranchName();
// Compare the hash of the tree object at the top of the tree that
// was just written with the hash of the tree object that the
// `HEAD` commit points at. If they are the same, abort because
// there is nothing new to commit.
if (refs.hash("HEAD") !== undefined &&
treeHash === objects.treeHash(objects.read(refs.hash("HEAD")))) {
throw new Error("# On " + headDesc + "\nnothing to commit, working directory clean");
} else {
// Abort if the repository is in the merge state and there are
// unresolved merge conflicts.
var conflictedPaths = index.conflictedPaths();
if (merge.isMergeInProgress() && conflictedPaths.length > 0) {
throw new Error(conflictedPaths.map(function(p) { return "U " + p; }).join("\n") +
"\ncannot commit because you have unmerged files\n");
// Otherwise, do the commit.
} else {
// If the repository is in the merge state, use a pre-written
// merge commit message. If the repository is not in the
// merge state, use the message passed with `-m`.
var m = merge.isMergeInProgress() ? files.read(files.gitletPath("MERGE_MSG")) : opts.m;
// Write the new commit to the `objects` directory.
var commitHash = objects.writeCommit(treeHash, m, refs.commitParentHashes());
// Point `HEAD` at new commit.
gitlet.update_ref("HEAD", commitHash);
// If `MERGE_HEAD` exists, the repository was in the merge
// state. Remove `MERGE_HEAD` and `MERGE_MSG`to exit the merge
// state. Report that the merge is complete.
if (merge.isMergeInProgress()) {
fs.unlinkSync(files.gitletPath("MERGE_MSG"));
refs.rm("MERGE_HEAD");
return "Merge made by the three-way strategy";
// Repository was not in the merge state, so just report that
// the commit is complete.
} else {
return "[" + headDesc + " " + commitHash + "] " + m;
}
}
}
},
// **branch()** creates a new branch that points at the commit that
// `HEAD` points at.
branch: function(name, opts) {
files.assertInRepo();
opts = opts || {};
// If no branch `name` was passed, list the local branches.
if (name === undefined) {
return Object.keys(refs.localHeads()).map(function(branch) {
return (branch === refs.headBranchName() ? "* " : " ") + branch;
}).join("\n") + "\n";
// `HEAD` is not pointing at a commit, so there is no commit for
// the new branch to point at. Abort. This is most likely to
// happen if the repository has no commits.
} else if (refs.hash("HEAD") === undefined) {
throw new Error(refs.headBranchName() + " not a valid object name");
// Abort because a branch called `name` already exists.
} else if (refs.exists(refs.toLocalRef(name))) {
throw new Error("A branch named " + name + " already exists");
// Otherwise, create a new branch by creating a new file called
// `name` that contains the hash of the commit that `HEAD` points
// at.
} else {
gitlet.update_ref(refs.toLocalRef(name), refs.hash("HEAD"));
}
},
// **checkout()** changes the index, working copy and `HEAD` to
// reflect the content of `ref`. `ref` might be a branch name or a
// commit hash.
checkout: function(ref, _) {
files.assertInRepo();
config.assertNotBare();
// Get the hash of the commit to check out.
var toHash = refs.hash(ref);
// Abort if `ref` cannot be found.
if (!objects.exists(toHash)) {
throw new Error(ref + " did not match any file(s) known to Gitlet");
// Abort if the hash to check out points to an object that is a
// not a commit.
} else if (objects.type(objects.read(toHash)) !== "commit") {
throw new Error("reference is not a tree: " + ref);
// Abort if `ref` is the name of the branch currently checked out.
// Abort if head is detached, `ref` is a commit hash and `HEAD` is
// pointing at that hash.
} else if (ref === refs.headBranchName() ||
ref === files.read(files.gitletPath("HEAD"))) {
return "Already on " + ref;
} else {
// Get a list of files changed in the working copy. Get a list
// of the files that are different in the head commit and the
// commit to check out. If any files appear in both lists then
// abort.
var paths = diff.changedFilesCommitWouldOverwrite(toHash);
if (paths.length > 0) {
throw new Error("local changes would be lost\n" + paths.join("\n") + "\n");
// Otherwise, perform the checkout.
} else {
process.chdir(files.workingCopyPath());
// If the ref is in the `objects` directory, it must be a hash
// and so this checkout is detaching the head.
var isDetachingHead = objects.exists(ref);
// Get the list of differences between the current commit and
// the commit to check out. Write them to the working copy.
workingCopy.write(diff.diff(refs.hash("HEAD"), toHash));
// Write the commit being checked out to `HEAD`. If the head
// is being detached, the commit hash is written directly to
// the `HEAD` file. If the head is not being detached, the
// branch being checked out is written to `HEAD`.
refs.write("HEAD", isDetachingHead ? toHash : "ref: " + refs.toLocalRef(ref));
// Set the index to the contents of the commit being checked
// out.
index.write(index.tocToIndex(objects.commitToc(toHash)));
// Report the result of the checkout.
return isDetachingHead ?
"Note: checking out " + toHash + "\nYou are in detached HEAD state." :
"Switched to branch " + ref;
}
}
},
// **diff()** shows the changes required to go from the `ref1`
// commit to the `ref2` commit.
diff: function(ref1, ref2, opts) {
files.assertInRepo();
config.assertNotBare();
// Abort if `ref1` was supplied, but it does not resolve to a
// hash.
if (ref1 !== undefined && refs.hash(ref1) === undefined) {
throw new Error("ambiguous argument " + ref1 + ": unknown revision");
// Abort if `ref2` was supplied, but it does not resolve to a
// hash.
} else if (ref2 !== undefined && refs.hash(ref2) === undefined) {
throw new Error("ambiguous argument " + ref2 + ": unknown revision");
// Otherwise, perform diff.
} else {
// Gitlet only shows the name of each changed file and whether
// it was added, modified or deleted. For simplicity, the
// changed content is not shown.
// The diff happens between two versions of the repository. The
// first version is either the hash that `ref1` resolves to, or
// the index. The second version is either the hash that `ref2`
// resolves to, or the working copy.
var nameToStatus = diff.nameStatus(diff.diff(refs.hash(ref1), refs.hash(ref2)));
// Show the path of each changed file.
return Object.keys(nameToStatus)
.map(function(path) { return nameToStatus[path] + " " + path; })
.join("\n") + "\n";
}
},
// **remote()** records the locations of remote versions of this
// repository.
remote: function(command, name, path, _) {
files.assertInRepo();
// Abort if `command` is not "add". Only "add" is supported.
if (command !== "add") {
throw new Error("unsupported");
// Abort if repository already has a record for a remote called
// `name`.
} else if (name in config.read()["remote"]) {
throw new Error("remote " + name + " already exists");
// Otherwise, add remote record.
} else {
// Write to the config file a record of the `name` and `path` of
// the remote.
config.write(util.setIn(config.read(), ["remote", name, "url", path]));
return "\n";
}
},
// **fetch()** records the commit that `branch` is at on `remote`.
// It does not change the local branch.
fetch: function(remote, branch, _) {
files.assertInRepo();
// Abort if a `remote` or `branch` not passed.
if (remote === undefined || branch === undefined) {
throw new Error("unsupported");
// Abort if `remote` not recorded in config file.
} else if (!(remote in config.read().remote)) {
throw new Error(remote + " does not appear to be a git repository");
} else {
// Get the location of the remote.
var remoteUrl = config.read().remote[remote].url;
// Turn the unqualified branch name into a qualified remote ref
// eg `[branch] -> refs/remotes/[remote]/[branch]`
var remoteRef = refs.toRemoteRef(remote, branch);
// Go to the remote repository and get the hash of the commit
// that `branch` is on.
var newHash = util.onRemote(remoteUrl)(refs.hash, branch);
// Abort if `branch` did not exist on the remote.
if (newHash === undefined) {
throw new Error("couldn't find remote ref " + branch);
// Otherwise, perform the fetch.
} else {
// Note down the hash of the commit this repository currently
// thinks the remote branch is on.
var oldHash = refs.hash(remoteRef);
// Get all the objects in the remote `objects` directory and
// write them. to the local `objects` directory. (This is an
// inefficient way of getting all the objects required to
// recreate locally the commit the remote branch is on.)
var remoteObjects = util.onRemote(remoteUrl)(objects.allObjects);
remoteObjects.forEach(objects.write);
// Set the contents of the file at
// `.gitlet/refs/remotes/[remote]/[branch]` to `newHash`, the
// hash of the commit that the remote branch is on.
gitlet.update_ref(remoteRef, newHash);
// Record the hash of the commit that the remote branch is on
// in `FETCH_HEAD`. (The user can call `gitlet merge
// FETCH_HEAD` to merge the remote version of the branch into
// their local branch. For more details, see
// [gitlet.merge()](#section-93).)
refs.write("FETCH_HEAD", newHash + " branch " + branch + " of " + remoteUrl);
// Report the result of the fetch.
return ["From " + remoteUrl,
"Count " + remoteObjects.length,
branch + " -> " + remote + "/" + branch +
(merge.isAForceFetch(oldHash, newHash) ? " (forced)" : "")].join("\n") + "\n";
}
}
},
// **merge()** finds the set of differences between the commit that
// the currently checked out branch is on and the commit that `ref`
// points to. It finds or creates a commit that applies these
// differences to the checked out branch.
merge: function(ref, _) {
files.assertInRepo();
config.assertNotBare();
// Get the `receiverHash`, the hash of the commit that the
// current branch is on.
var receiverHash = refs.hash("HEAD");
// Get the `giverHash`, the hash for the commit to merge into the
// receiver commit.
var giverHash = refs.hash(ref);
// Abort if head is detached. Merging into a detached head is not
// supported.
if (refs.isHeadDetached()) {
throw new Error("unsupported");
// Abort if `ref` did not resolve to a hash, or if that hash is
// not for a commit object.
} else if (giverHash === undefined || objects.type(objects.read(giverHash)) !== "commit") {
throw new Error(ref + ": expected commit type");
// Do not merge if the current branch - the receiver - already has
// the giver's changes. This is the case if the receiver and
// giver are the same commit, or if the giver is an ancestor of
// the receiver.
} else if (objects.isUpToDate(receiverHash, giverHash)) {
return "Already up-to-date";
} else {
// Get a list of files changed in the working copy. Get a list
// of the files that are different in the receiver and giver. If
// any files appear in both lists then abort.
var paths = diff.changedFilesCommitWouldOverwrite(giverHash);
if (paths.length > 0) {
throw new Error("local changes would be lost\n" + paths.join("\n") + "\n");
// If the receiver is an ancestor of the giver, a fast forward
// is performed. This is possible because there is already a
// commit that incorporates all of the giver's changes into the
// receiver.
} else if (merge.canFastForward(receiverHash, giverHash)) {
// Fast forwarding means making the current branch reflect the
// commit that `giverHash` points at. The branch is pointed
// at `giverHash`. The index is set to match the contents of
// the commit that `giverHash` points at. The working copy is
// set to match the contents of that commit.
merge.writeFastForwardMerge(receiverHash, giverHash);
return "Fast-forward";
// If the receiver is not an ancestor of the giver, a merge
// commit must be created.
} else {
// The repository is put into the merge state. The
// `MERGE_HEAD` file is written and its contents set to
// `giverHash`. The `MERGE_MSG` file is written and its
// contents set to a boilerplate merge commit message. A
// merge diff is created that will turn the contents of
// receiver into the contents of giver. This contains the
// path of every file that is different and whether it was
// added, removed or modified, or is in conflict. Added files
// are added to the index and working copy. Removed files are
// removed from the index and working copy. Modified files
// are modified in the index and working copy. Files that are
// in conflict are written to the working copy to include the
// receiver and giver versions. Both the receiver and giver
// versions are written to the index.
merge.writeNonFastForwardMerge(receiverHash, giverHash, ref);
// If there are any conflicted files, a message is shown to
// say that the user must sort them out before the merge can
// be completed.
if (merge.hasConflicts(receiverHash, giverHash)) {
return "Automatic merge failed. Fix conflicts and commit the result.";
// If there are no conflicted files, a commit is created from
// the merged changes and the merge is over.
} else {
return gitlet.commit();
}
}
}
},
// **pull()** fetches the commit that `branch` is on at `remote`.
// It merges that commit into the current branch.
pull: function(remote, branch, _) {
files.assertInRepo();
config.assertNotBare();
gitlet.fetch(remote, branch);
return gitlet.merge("FETCH_HEAD");
},
// **push()** gets the commit that `branch` is on in the local repo
// and points `branch` on `remote` at the same commit.
push: function(remote, branch, opts) {
files.assertInRepo();
opts = opts || {};
// Abort if a `remote` or `branch` not passed.
if (remote === undefined || branch === undefined) {
throw new Error("unsupported");
// Abort if `remote` not recorded in config file.
} else if (!(remote in config.read().remote)) {
throw new Error(remote + " does not appear to be a git repository");
} else {
var remotePath = config.read().remote[remote].url;
var remoteCall = util.onRemote(remotePath);
// Abort if remote repository is not bare and `branch` is
// checked out.
if (remoteCall(refs.isCheckedOut, branch)) {
throw new Error("refusing to update checked out branch " + branch);
} else {
// Get `receiverHash`, the hash of the commit that `branch` is
// on at `remote`.
var receiverHash = remoteCall(refs.hash, branch);
// Get `giverHash`, the hash of the commit that `branch` is on
// at the local repository.
var giverHash = refs.hash(branch);
// Do nothing if the remote branch - the receiver - has
// already incorporated the commit that `giverHash` points
// to. This is the case if the receiver commit and giver
// commit are the same, or if the giver commit is an ancestor
// of the receiver commit.
if (objects.isUpToDate(receiverHash, giverHash)) {
return "Already up-to-date";
// Abort if `branch` on `remote` cannot be fast forwarded to
// the commit that `giverHash` points to. A fast forward can
// only be done if the receiver commit is an ancestor of the
// giver commit.
} else if (!opts.f && !merge.canFastForward(receiverHash, giverHash)) {
throw new Error("failed to push some refs to " + remotePath);
// Otherwise, do the push.
} else {
// Put all the objects in the local `objects` directory into
// the remote `objects` directory.
objects.allObjects().forEach(function(o) { remoteCall(objects.write, o); });
// Point `branch` on `remote` at `giverHash`.
remoteCall(gitlet.update_ref, refs.toLocalRef(branch), giverHash);
// Set the local repo's record of what commit `branch` is on
// at `remote` to `giverHash` (since that is what it is now
// is).
gitlet.update_ref(refs.toRemoteRef(remote, branch), giverHash);
// Report the result of the push.
return ["To " + remotePath,
"Count " + objects.allObjects().length,
branch + " -> " + branch].join("\n") + "\n";
}
}
}
},
// **status()** reports the state of the repo: the current branch,
// untracked files, conflicted files, files that are staged to be
// committed and files that are not staged to be committed.
status: function(_) {
files.assertInRepo();
config.assertNotBare();
return status.toString();
},
// **clone()** copies the repository at `remotePath` to
// **`targetPath`.
clone: function(remotePath, targetPath, opts) {
opts = opts || {};
// Abort if a `remotePath` or `targetPath` not passed.
if (remotePath === undefined || targetPath === undefined) {
throw new Error("you must specify remote path and target path");
// Abort if `remotePath` does not exist, or is not a Gitlet
// repository.
} else if (!fs.existsSync(remotePath) || !util.onRemote(remotePath)(files.inRepo)) {
throw new Error("repository " + remotePath + " does not exist");
// Abort if `targetPath` exists and is not empty.
} else if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
throw new Error(targetPath + " already exists and is not empty");
// Otherwise, do the clone.
} else {
remotePath = nodePath.resolve(process.cwd(), remotePath);
// If `targetPath` doesn't exist, create it.
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath);
}
// In the directory for the new remote repository...
util.onRemote(targetPath)(function() {
// Initialize the directory as a Gitlet repository.
gitlet.init(opts);
// Set up `remotePath` as a remote called "origin".
gitlet.remote("add", "origin", nodePath.relative(process.cwd(), remotePath));
// Get the hash of the commit that master is pointing at on
// the remote repository.
var remoteHeadHash = util.onRemote(remotePath)(refs.hash, "master");
// If the remote repo has any commits, that hash will exist.
// The new repository records the commit that the passed
// `branch` is at on the remote. It then sets master on the
// new repository to point at that commit.
if (remoteHeadHash !== undefined) {
gitlet.fetch("origin", "master");
merge.writeFastForwardMerge(undefined, remoteHeadHash);
}
});
// Report the result of the clone.
return "Cloning into " + targetPath;
}
},
// **update_index()** adds the contents of the file at `path` to the
// index, or removes the file from the index.
update_index: function(path, opts) {
files.assertInRepo();
config.assertNotBare();
opts = opts || {};
var pathFromRoot = files.pathFromRepoRoot(path);
var isOnDisk = fs.existsSync(path);
var isInIndex = index.hasFile(path, 0);
// Abort if `path` is a directory. `update_index()` only handles
// single files.
if (isOnDisk && fs.statSync(path).isDirectory()) {
throw new Error(pathFromRoot + " is a directory - add files inside\n");
} else if (opts.remove && !isOnDisk && isInIndex) {
// Abort if file is being removed and is in conflict. Gitlet
// doesn't support this.
if (index.isFileInConflict(path)) {
throw new Error("unsupported");
// If files is being removed, is not on disk and is in the
// index, remove it from the index.
} else {
index.writeRm(path);
return "\n";
}
// If file is being removed, is not on disk and not in the index,
// there is no work to do.
} else if (opts.remove && !isOnDisk && !isInIndex) {
return "\n";
// Abort if the file is on disk and not in the index and the
// `--add` was not passed.
} else if (!opts.add && isOnDisk && !isInIndex) {
throw new Error("cannot add " + pathFromRoot + " to index - use --add option\n");
// If file is on disk and either `-add` was passed or the file is
// in the index, add the file's current content to the index.
} else if (isOnDisk && (opts.add || isInIndex)) {
index.writeNonConflict(path, files.read(files.workingCopyPath(path)));
return "\n";
// Abort if the file is not on disk and `--remove` not passed.
} else if (!opts.remove && !isOnDisk) {
throw new Error(pathFromRoot + " does not exist and --remove not passed\n");
}
},
// **write_tree()** takes the content of the index and stores a tree
// object that represents that content to the `objects` directory.
write_tree: function(_) {
files.assertInRepo();
return objects.writeTree(files.nestFlatTree(index.toc()));
},
// **update_ref()** gets the hash of the commit that `refToUpdateTo`
// points at and sets `refToUpdate` to point at the same hash.
update_ref: function(refToUpdate, refToUpdateTo, _) {
files.assertInRepo();
// Get the hash that `refToUpdateTo` points at.
var hash = refs.hash(refToUpdateTo);
// Abort if `refToUpdateTo` does not point at a hash.
if (!objects.exists(hash)) {
throw new Error(refToUpdateTo + " not a valid SHA1");
// Abort if `refToUpdate` does not match the syntax of a ref.
} else if (!refs.isRef(refToUpdate)) {
throw new Error("cannot lock the ref " + refToUpdate);
// Abort if `hash` points to an object in the `objects` directory
// that is not a commit.
} else if (objects.type(objects.read(hash)) !== "commit") {
var branch = refs.terminalRef(refToUpdate);
throw new Error(branch + " cannot refer to non-commit object " + hash + "\n");
// Otherwise, set the contents of the file that the ref represents
// to `hash`.
} else {
refs.write(refs.terminalRef(refToUpdate), hash);
}
}
};
// Refs module
// -----------
// Refs are names for commit hashes. The ref is the name of a file.
// Some refs represent local branches, like `refs/heads/master` or
// `refs/heads/feature`. Some represent remote branches, like
// `refs/remotes/origin/master`. Some represent important states of
// the repository, like `HEAD`, `MERGE_HEAD` and `FETCH_HEAD`. Ref
// files contain either a hash or another ref.
var refs = {
// **isRef()** returns true if `ref` matches valid qualified ref
// syntax.
isRef: function(ref) {
return ref !== undefined &&
(ref.match("^refs/heads/[A-Za-z-]+$") ||
ref.match("^refs/remotes/[A-Za-z-]+/[A-Za-z-]+$") ||
["HEAD", "FETCH_HEAD", "MERGE_HEAD"].indexOf(ref) !== -1);
},
// **terminalRef()** resolves `ref` to the most specific ref
// possible.
terminalRef: function(ref) {
// If `ref` is "HEAD" and head is pointing at a branch, return the
// branch.
if (ref === "HEAD" && !refs.isHeadDetached()) {
return files.read(files.gitletPath("HEAD")).match("ref: (refs/heads/.+)")[1];
// If ref is qualified, return it.
} else if (refs.isRef(ref)) {
return ref;
// Otherwise, assume ref is an unqualified local ref (like
// `master`) and turn it into a qualified ref (like
// `refs/heads/master`)
} else {
return refs.toLocalRef(ref);
}
},
// **hash()** returns the hash that `refOrHash` points to.
hash: function(refOrHash) {
if (objects.exists(refOrHash)) {
return refOrHash;
} else {
var terminalRef = refs.terminalRef(refOrHash);
if (terminalRef === "FETCH_HEAD") {
return refs.fetchHeadBranchToMerge(refs.headBranchName());
} else if (refs.exists(terminalRef)) {
return files.read(files.gitletPath(terminalRef));
}
}
},
// **isHeadDetached()** returns true if `HEAD` contains a commit
// hash, rather than the ref of a branch.
isHeadDetached: function() {
return files.read(files.gitletPath("HEAD")).match("refs") === null;
},
// **isCheckedOut()** returns true if the repository is not bare and
// `HEAD` is pointing at the branch called `branch`
isCheckedOut: function(branch) {
return !config.isBare() && refs.headBranchName() === branch;
},
// **toLocalRef()** converts the branch name `name` into a qualified
// local branch ref.
toLocalRef: function(name) {
return "refs/heads/" + name;
},
// **toRemoteRef()** converts `remote` and branch name `name` into a
// qualified remote branch ref.
toRemoteRef: function(remote, name) {
return "refs/remotes/" + remote + "/" + name;
},
// **write()** sets the content of the file for the qualified ref
// `ref` to `content`.
write: function(ref, content) {
if (refs.isRef(ref)) {
files.write(files.gitletPath(nodePath.normalize(ref)), content);
}
},
// **rm()** removes the file for the qualified ref `ref`.
rm: function(ref) {
if (refs.isRef(ref)) {
fs.unlinkSync(files.gitletPath(ref));
}
},
// **fetchHeadBranchToMerge()** reads the `FETCH_HEAD` file and gets
// the hash that the remote `branchName` is pointing at. For more
// information about `FETCH_HEAD` see [gitlet.fetch()](#section-80).
fetchHeadBranchToMerge: function(branchName) {
return util.lines(files.read(files.gitletPath("FETCH_HEAD")))
.filter(function(l) { return l.match("^.+ branch " + branchName + " of"); })
.map(function(l) { return l.match("^([^ ]+) ")[1]; })[0];
},
// **localHeads()** returns a JS object that maps local branch names
// to the hash of the commit they point to.
localHeads: function() {
return fs.readdirSync(nodePath.join(files.gitletPath(), "refs", "heads"))
.reduce(function(o, n) { return util.setIn(o, [n, refs.hash(n)]); }, {});
},
// **exists()** returns true if the qualified ref `ref` exists.
exists: function(ref) {
return refs.isRef(ref) && fs.existsSync(files.gitletPath(ref));
},
// **headBranchName()** returns the name of the branch that `HEAD`
// is pointing at.
headBranchName: function() {
if (!refs.isHeadDetached()) {
return files.read(files.gitletPath("HEAD")).match("refs/heads/(.+)")[1];
}
},
// **commitParentHashes()** returns the array of commits that would
// be the parents of the next commit.
commitParentHashes: function() {
var headHash = refs.hash("HEAD");
// If the repository is in the middle of a merge, return the
// hashes of the two commits being merged.
if (merge.isMergeInProgress()) {
return [headHash, refs.hash("MERGE_HEAD")];
// If this repository has no commits, return an empty array.
} else if (headHash === undefined) {
return [];
// Otherwise, return the hash of the commit that `HEAD` is
// currently pointing at.
} else {
return [headHash];
}
}
};
// Objects module
// -----------
// Objects are files in the `.gitlet/objects/` directory.
// - A blob object stores the content of a file. For example, if a
// file called `numbers.txt` that contains `first` is added to the
// index, a blob called `hash(first)` will be created containing
// `"first"`.
// - A tree object stores a list of files and directories in a
// directory in the repository. Entries in the list for files point
// to blob objects. Entries in the list for directories point at
// other tree objects.
// - A commit object stores a pointer to a tree object and a message.
// It represents the state of the repository after a commit.
var objects = {
// **writeTree()** stores a graph of tree objects that represent the
// content currently in the index.
writeTree: function(tree) {
var treeObject = Object.keys(tree).map(function(key) {
if (util.isString(tree[key])) {
return "blob " + tree[key] + " " + key;
} else {
return "tree " + objects.writeTree(tree[key]) + " " + key;
}
}).join("\n") + "\n";
return objects.write(treeObject);
},
// **fileTree()** takes a tree hash and finds the corresponding tree
// object. It reads the connected graph of tree objects into a
// nested JS object, like:<br/>
// `{ file1: "hash(1)", src: { file2: "hash(2)" }`
fileTree: function(treeHash, tree) {
if (tree === undefined) { return objects.fileTree(treeHash, {}); }
util.lines(objects.read(treeHash)).forEach(function(line) {
var lineTokens = line.split(/ /);
tree[lineTokens[2]] = lineTokens[0] === "tree" ?
objects.fileTree(lineTokens[1], {}) :
lineTokens[1];
});
return tree;
},
// **writeCommit()** creates a commit object and writes it to the
// objects database.
writeCommit: function(treeHash, message, parentHashes) {
return objects.write("commit " + treeHash + "\n" +