[project] Switch snapshot symlink to hardlink

Makes the build deterministic across multiple checkouts by hard linking
to the timestamp named snapshot file instead of symlinking.

Test: Manually ran `jiri update`, verified that `latest` was a hard link
to the latest snapshot. Then ran `jiri update` again and verified that
`second-latest` was not the latest.

Bug: 64127
Change-Id: Iedcec385281995a79d0a2a9b3a9ba82c0857a5f9
Reviewed-on: https://fuchsia-review.googlesource.com/c/jiri/+/483537
Commit-Queue: Anirudh Mathukumilli <rudymathu@google.com>
Reviewed-by: Haowei Wu <haowei@google.com>
diff --git a/project/project.go b/project/project.go
index fcbe020..205019f 100644
--- a/project/project.go
+++ b/project/project.go
@@ -1463,34 +1463,25 @@
 
 	latestLink, secondLatestLink := jirix.UpdateHistoryLatestLink(), jirix.UpdateHistorySecondLatestLink()
 
-	// If the "latest" symlink exists, point the "second-latest" symlink to its value.
+	// If the "latest" hardlink exists, point the "second-latest" hardlink to its value.
 	latestLinkExists, err := isFile(latestLink)
 	if err != nil {
 		return err
 	}
 	if latestLinkExists {
-		latestFile, err := os.Readlink(latestLink)
-		if err != nil {
-			return fmtError(err)
-		}
 		if err := os.RemoveAll(secondLatestLink); err != nil {
 			return fmtError(err)
 		}
-		if err := os.Symlink(latestFile, secondLatestLink); err != nil {
+		if err := os.Link(latestLink, secondLatestLink); err != nil {
 			return fmtError(err)
 		}
 	}
 
-	// Point the "latest" update history symlink to the new snapshot file.  Try
-	// to keep the symlink relative, to make it easy to move or copy the entire
-	// update_history directory.
-	if rel, err := filepath.Rel(filepath.Dir(latestLink), snapshotFile); err == nil {
-		snapshotFile = rel
-	}
+	// Point the "latest" update history hardlink to the new snapshot file.
 	if err := os.RemoveAll(latestLink); err != nil {
 		return fmtError(err)
 	}
-	return fmtError(os.Symlink(snapshotFile, latestLink))
+	return fmtError(os.Link(snapshotFile, latestLink))
 }
 
 // CleanupProjects restores the given jiri projects back to their detached
diff --git a/x.go b/x.go
index 9d5031f..fcd3a4f 100644
--- a/x.go
+++ b/x.go
@@ -463,13 +463,13 @@
 	return filepath.Join(x.RootMetaDir(), "update_history")
 }
 
-// UpdateHistoryLatestLink returns the path to a symlink that points to the
+// UpdateHistoryLatestLink returns the path to a hard link that points to the
 // latest update in the update history directory.
 func (x *X) UpdateHistoryLatestLink() string {
 	return filepath.Join(x.UpdateHistoryDir(), "latest")
 }
 
-// UpdateHistorySecondLatestLink returns the path to a symlink that points to
+// UpdateHistorySecondLatestLink returns the path to a hard link that points to
 // the second latest update in the update history directory.
 func (x *X) UpdateHistorySecondLatestLink() string {
 	return filepath.Join(x.UpdateHistoryDir(), "second-latest")