All the code for the articles in this series can be found here.
As with the previous article, we’ll now look at an extension to my original programming assignment. This time we’ll teach DVCSUS about remote repositories.
Interface
One of the cornerstones of distributed source control is the ability to interact with distant repositories. This is crucial because it enables teams to cooperate by having a common remote repository where they can push or pull modifications. Generally speaking, such a repository is hosted on a network or the Internet (common hosting platforms includes Github and Bitbucket). In the case of DVCSUS, since it’s only a prototype, we’ll only offer the possiblity to set a remote using a file path. This will bar DVCSUS from interacting with a repository hosted on the Internet but it will still enable us to interact with a remote repository through a network drive.
Moreover, we’ll also provide users with command to push and pull information from the remote repository. Again, we want DVCSUS to stay as simple as possible. This is why the push
and pull
commands won’t take any parameters. The remote repository they’ll interact with will be the one previously set and no other. If no remote is set, then the commands won’t do anything.
All in all, we’ll offer the following interface:
1
2
3
dvcsus set_remote <repopath>
dvcsus push
dvcsus pull
Remote Repository As Metadata
As the remote repository is something that can be set locally, it goes without saying that this information needs to go into the Staging
database. As was shown in earlier articles, this database contains a Metadata
table where we can store any details we want about the local state of the repository. Thus, it’s the perfect place to store the remote repository path.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[nodiscard]] bool SetRemote(const fs::path &remoteRepoPath) noexcept
{
try
{
const auto setRemoteQuery{fmt::format("..."), // Omitted for brevity
fs::absolute(remoteRepoPath).string())};
return ExecuteQuery(STAGING_DB_PATH, setRemoteQuery);
}
catch (const std::exception &e)
{
fmt::print(std::cerr, "{}\n", e.what());
return false;
}
}
Take note that we make sure to store an absolute path in the database. While there’s no problem accepting a relative path as an argument, storing it as such might become a problem later on. The fact is that a relative path could become invalid if the user moves the repository on disk. This would then negatively impact the push
and pull
commands.
Transfering Data
Linking a local repository with a remote one was the easy part. Now comes the les easy part: transfering data to and from the remote repository.
Yet, it’s not actually that difficult once we take into account two observations:
- For our prototype, the transfer operation is symetrical. In other words, the operation stays the same when pulling or pushing; it’s only the source and destination that change.
- DVCSUS doesn’t allow rewriting the past. Therefore, we can only insert new data in the
Repository
database but never remove it. This effectively means that transfering data can be implemented in terms ofINSERT OR IGNORE
statements. Everything that’s in common between twoRepository
databases will remain untouched and what’s missing from the destination database will be copied from the source database.
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
enum class TransferDirection { ToLocal, ToRemote };
[[nodiscard]] bool Transfer(TransferDirection direction) noexcept
{
try
{
fs::path source;
fs::path destination;
switch (direction)
{
case TransferDirection::ToLocal:
source = GetRemote();
destination = fs::current_path() / dvcs::REPO_DB_PATH;
break;
case TransferDirection::ToRemote:
destination = GetRemote();
source = fs::current_path() / dvcs::REPO_DB_PATH;
break;
default:
fmt::print(std::cerr, "Unsupported transfer option\n");
return false;
}
RETURN_IF(source.empty(), false);
RETURN_IF(destination.empty(), false);
const auto query{
fmt::format("...", // Lots of INSERT OR IGNORE omitted for brevity
source.string())};
return ExecuteQuery(destination, query);
}
catch (const std::exception &e)
{
fmt::print(std::cerr, "{}\n", e.what());
return false;
}
}
Push And Pull
After the transfer mechanism has been put in place, the push
and pull
commands are trivially easy to implement:
1
2
[[nodiscard]] bool Pull() noexcept { return Transfer(TransferDirection::ToLocal); }
[[nodiscard]] bool Push() noexcept { return Transfer(TransferDirection::ToRemote); }
This is a simple as it gets.
What About Conflicts?
Good question! In the idealized world of our prototype, there’s no such thing as conflicts!