Commit 36f784b538c4b27f7b52427d2cfce06c535abba0

Carlos Martín Nieto 2015-06-01T20:02:23

config: expose locking via the main API This lock/unlock pair allows for the cller to lock a configuration file to avoid concurrent operations. It also allows for a transactional approach to updating a configuration file. If multiple updates must be made atomically, they can be done while the config is locked.

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 243b696..4442d0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ v0.23 + 1
 
 ### API additions
 
+* `git_config_lock()` and `git_config_unlock()` have been added, which
+  allow for transactional/atomic complex updates to the configuration,
+  removing the opportunity for concurrent operations and not
+  committing any changes until the unlock.
+
+
 ### API removals
 
 ### Breaking API changes
@@ -19,6 +25,10 @@ v0.23 + 1
   with the reflog on ref deletion. The file-based backend must delete
   it, a database-backed one may wish to archive it.
 
+* `git_config_backend` has gained two entries. `lock` and `unlock`
+  with which to implement the transactional/atomic semantics for the
+  configuration backend.
+
 v0.23
 ------
 
diff --git a/include/git2/config.h b/include/git2/config.h
index 6d3fdb0..2550f8e 100644
--- a/include/git2/config.h
+++ b/include/git2/config.h
@@ -689,6 +689,33 @@ GIT_EXTERN(int) git_config_backend_foreach_match(
 	void *payload);
 
 
+/**
+ * Lock the backend with the highest priority
+ *
+ * Locking disallows anybody else from writing to that backend. Any
+ * updates made after locking will not be visible to a reader until
+ * the file is unlocked.
+ *
+ * @param cfg the configuration in which to lock
+ * @return 0 or an error code
+ */
+GIT_EXTERN(int) git_config_lock(git_config *cfg);
+
+/**
+ * Unlock the backend with the highest priority
+ *
+ * Unlocking will allow other writers to updat the configuration
+ * file. Optionally, any changes performed since the lock will be
+ * applied to the configuration.
+ *
+ * @param cfg the configuration
+ * @param commit boolean which indicates whether to commit any changes
+ * done since locking
+ * @return 0 or an error code
+ */
+GIT_EXTERN(int) git_config_unlock(git_config *cfg, int commit);
+
+
 /** @} */
 GIT_END_DECL
 #endif
diff --git a/src/config.c b/src/config.c
index 77cf573..937d00d 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1144,6 +1144,37 @@ int git_config_open_default(git_config **out)
 	return error;
 }
 
+int git_config_lock(git_config *cfg)
+{
+	git_config_backend *file;
+	file_internal *internal;
+
+	internal = git_vector_get(&cfg->files, 0);
+	if (!internal || !internal->file) {
+		giterr_set(GITERR_CONFIG, "cannot lock; the config has no backends/files");
+		return -1;
+	}
+	file = internal->file;
+
+	return file->lock(file);
+}
+
+int git_config_unlock(git_config *cfg, int commit)
+{
+	git_config_backend *file;
+	file_internal *internal;
+
+	internal = git_vector_get(&cfg->files, 0);
+	if (!internal || !internal->file) {
+		giterr_set(GITERR_CONFIG, "cannot lock; the config has no backends/files");
+		return -1;
+	}
+
+	file = internal->file;
+
+	return file->unlock(file, commit);
+}
+
 /***********
  * Parsers
  ***********/
diff --git a/tests/config/write.c b/tests/config/write.c
index 5446b95..e43c26b 100644
--- a/tests/config/write.c
+++ b/tests/config/write.c
@@ -635,44 +635,47 @@ void test_config_write__to_file_with_only_comment(void)
 
 void test_config_write__locking(void)
 {
-	git_config_backend *cfg, *cfg2;
+	git_config *cfg, *cfg2;
 	git_config_entry *entry;
 	const char *filename = "locked-file";
 
 	/* Open the config and lock it */
 	cl_git_mkfile(filename, "[section]\n\tname = value\n");
-	cl_git_pass(git_config_file__ondisk(&cfg, filename));
-	cl_git_pass(git_config_file_open(cfg, GIT_CONFIG_LEVEL_APP));
-	cl_git_pass(git_config_file_get_string(&entry, cfg, "section.name"));
+	cl_git_pass(git_config_open_ondisk(&cfg, filename));
+	cl_git_pass(git_config_get_entry(&entry, cfg, "section.name"));
 	cl_assert_equal_s("value", entry->value);
 	git_config_entry_free(entry);
-	cl_git_pass(git_config_file_lock(cfg));
+	cl_git_pass(git_config_lock(cfg));
 
 	/* Change entries in the locked backend */
-	cl_git_pass(git_config_file_set_string(cfg, "section.name", "other value"));
-	cl_git_pass(git_config_file_set_string(cfg, "section2.name3", "more value"));
+	cl_git_pass(git_config_set_string(cfg, "section.name", "other value"));
+	cl_git_pass(git_config_set_string(cfg, "section2.name3", "more value"));
 
 	/* We can see that the file we read from hasn't changed */
-	cl_git_pass(git_config_file__ondisk(&cfg2, filename));
-	cl_git_pass(git_config_file_open(cfg2, GIT_CONFIG_LEVEL_APP));
-	cl_git_pass(git_config_file_get_string(&entry, cfg2, "section.name"));
+	cl_git_pass(git_config_open_ondisk(&cfg2, filename));
+	cl_git_pass(git_config_get_entry(&entry, cfg2, "section.name"));
 	cl_assert_equal_s("value", entry->value);
 	git_config_entry_free(entry);
-	cl_git_fail_with(GIT_ENOTFOUND, git_config_file_get_string(&entry, cfg2, "section2.name3"));
-	git_config_file_free(cfg2);
+	cl_git_fail_with(GIT_ENOTFOUND, git_config_get_entry(&entry, cfg2, "section2.name3"));
+	git_config_free(cfg2);
 
-	git_config_file_unlock(cfg, true);
-	git_config_file_free(cfg);
+	/* And we also get the old view when we read from the locked config */
+	cl_git_pass(git_config_get_entry(&entry, cfg, "section.name"));
+	cl_assert_equal_s("value", entry->value);
+	git_config_entry_free(entry);
+	cl_git_fail_with(GIT_ENOTFOUND, git_config_get_entry(&entry, cfg, "section2.name3"));
+
+	git_config_unlock(cfg, true);
+	git_config_free(cfg);
 
 	/* Now that we've unlocked it, we should see both updates */
-	cl_git_pass(git_config_file__ondisk(&cfg, filename));
-	cl_git_pass(git_config_file_open(cfg, GIT_CONFIG_LEVEL_APP));
-	cl_git_pass(git_config_file_get_string(&entry, cfg, "section.name"));
+	cl_git_pass(git_config_open_ondisk(&cfg, filename));
+	cl_git_pass(git_config_get_entry(&entry, cfg, "section.name"));
 	cl_assert_equal_s("other value", entry->value);
 	git_config_entry_free(entry);
-	cl_git_pass(git_config_file_get_string(&entry, cfg, "section2.name3"));
+	cl_git_pass(git_config_get_entry(&entry, cfg, "section2.name3"));
 	cl_assert_equal_s("more value", entry->value);
 	git_config_entry_free(entry);
 
-	git_config_file_free(cfg);
+	git_config_free(cfg);
 }