Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

feat(lib): add support for git repositories as data source #49

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e90377e
chore(root): adds git_cli dep
rockchalkwushock Oct 10, 2021
20eefa3
feat(lib): adds repository_provider and git modules
rockchalkwushock Oct 11, 2021
c477187
feat(config): adds remote repo to config
rockchalkwushock Oct 18, 2021
525e853
feat(lib): adds initial repository_watcher
rockchalkwushock Oct 18, 2021
34cf531
feat(application): adds repository_watcher to tree
rockchalkwushock Oct 18, 2021
1d3b9a8
fix(config): adds missing comma
rockchalkwushock Oct 18, 2021
fd195a8
feat(config): adds recheck param for remote
rockchalkwushock Oct 18, 2021
6fa654b
fix(application): fixes call to get_env
rockchalkwushock Oct 18, 2021
4f5e8c6
fix(git): fixes call to get_env
rockchalkwushock Oct 18, 2021
f5ac9c9
feat(repository_watcher): implements init/1
rockchalkwushock Oct 18, 2021
673aec8
feat(repository_watcher): implements fetching and polling
rockchalkwushock Oct 18, 2021
932d1e1
Removed unused callbacks and typs
alfredbaudisch Oct 24, 2021
c74db71
Updated sample base configuration
alfredbaudisch Oct 24, 2021
88cb69a
Ignore repository from tests
alfredbaudisch Oct 24, 2021
94b29b8
File parser uses the new is_path_hidden? utility, to correctly detect…
alfredbaudisch Oct 24, 2021
3f2ae53
Ignore hidden path and file events
alfredbaudisch Oct 24, 2021
b9679c2
Clone the repository inside PardallMarkdown's :root_path, since this …
alfredbaudisch Oct 24, 2021
477f4df
Start RepositoryWatcher only if :remote_repository_url is provided
alfredbaudisch Oct 24, 2021
c517ec1
Bump hex version
alfredbaudisch Oct 24, 2021
870279d
Documentation about RepositoryWatcher and Git watching support
alfredbaudisch Oct 24, 2021
0c529b8
Added a "Contributors" section
alfredbaudisch Oct 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions lib/pardall_markdown/repository_provider.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule PardallMarkdown.RepositoryProvider do
@moduledoc """
This implementation closely follows the implementation found in:
https://github.com/meddle0x53/blogit/blob/master/lib/blogit/repository_provider.ex
Many thanks to @meddle0x53 for this!
"""

@type repository :: term
@type provider :: module
@type fetch_result :: {:no_updates} | {:updates, [String.t()]}
@type timestamp :: String.t()

@type file_path :: String.t()
@type folder :: String.t()
@type file_read_result :: {:ok, binary} | {:error, File.posix()}

@type t :: %__MODULE__{repo: repository, provider: provider}
@enforce_keys :provider
defstruct [:repo, :provider]

@doc """
Invoked to get a representation value of the repository the provider manages.
The actual data represented by this struct should be updated to its
newest version first.
If for example the repository is remote, all the files in it should be
downloaded so their most recent versions are accessible.
This structure can be passed to other callbacks in order to manage files
in the repository.
"""
@callback repository() :: repository

@doc """
Invoked to update the data represented by the given `repository` to its most
recent version.
If, for example the repository is remote, all the files in it should be
downloaded so their most recent versions are accessible.
Returns the path to the changed files in the form of the tuple
`{:updates, list-of-paths}`. These paths should be paths to deleted, updated
or newly created files.
"""
@callback fetch(repository) :: fetch_result

@doc """
Invoked to get the path to the locally downloaded data. If the repository
is remote, it should have local copy or something like that.
"""
@callback local_path() :: String.t()

@doc """
Invoked to get a list of file paths of set of files contained in the locally
downloaded repository.
"""
@callback list_files(folder) :: [file_path]

@doc """
Checks if a file path is contained in the local version of the repository.
"""
@callback file_in?(file_path) :: boolean

@doc """
Returns file information for the file located at the given `file_path` in
the given `repository`. The result should be in the form of a map and should
be structured like this:
```
%{
"author" => the-file-author,
"created_at" => the-date-the-file-was-created-in-iso-8601-format,
"updated_at" => the-date-of-the-last-update-of-the-file-in-iso-8601-format
}
```
"""
@callback file_info(repository, file_path) :: %{atom => String.t() | timestamp}

@doc """
Invoked in order to read the contents of the file located at the given
`file_path`.
The second parameter can be a path to a folder relative to
`Blogit.RepositoryProvider.local_path/0` in which the given `file_path` should
exist.
"""
@callback read_file(file_path, folder) :: file_read_result
end
137 changes: 137 additions & 0 deletions lib/pardall_markdown/repository_providers/git.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule PardallMarkdown.RepositoryProviders.Git do
@moduledoc """
This implementation closely follows the implementation found in:
https://github.com/meddle0x53/blogit/blob/master/lib/blogit/settings.ex
Many thanks to @meddle0x53 for this!
"""
require Logger

@behaviour PardallMarkdown.RepositoryProvider

@repository_url Application.get_env(:pardall_markdown, :repository_url, "")
@local_path @repository_url
|> String.split("/")
|> List.last()
|> String.trim_trailing(".git")

# Callbacks

def repository do
repo = git_repository()

case Git.pull(repo) do
{:ok, msg} ->
Logger.info("Pulling from git repository #{msg}")

{_, error} ->
Logger.error("Error while pulling from git repository #{inspect(error)}")
end

repo
end

def fetch(repo) do
Logger.info("Fetching data from #{@repository_url}")

case Git.fetch(repo) do
{:error, _} ->
{:no_updates}

{:ok, ""} ->
{:no_updates}

{:ok, _} ->
updates =
repo
|> Git.diff!(["--name-only", "HEAD", "origin/master"])
|> String.split("\n", trim: true)
|> Enum.map(&String.trim/1)

Logger.info("There are new updates, pulling them.")
Git.pull!(repo)

{:updates, updates}
end
end

def local_path, do: @local_path

def list_files(folder \\ Settings.posts_folder()) do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alfredbaudisch So my thoughts here are we can default to the root of the repository and document that in the docs. We can then do one of two things:

  1. Add another optional setting in the config for the git_repo_data_path,
  2. Have root_path accept a different data type, perhaps a keyword list would be a good option.

For example my website is written in NextJS with MDX all the md(x) files live at data:

config :pardall_markdown, PardallMarkdown.Content,
  root_path: ["https://github.com/rockchalkwushock/codybrunner.dev.git", "data"]

What are your thoughts on how to expose this API to the end user?

path = Path.join(@local_path, folder)
size = byte_size(path) + 1

path
|> recursive_ls()
|> Enum.map(fn <<_::binary-size(size), rest::binary>> -> rest end)
end

def file_in?(file), do: File.exists?(Path.join(@local_path, file))

def file_info(repository, file_path) do
%{
author: file_author(repository, file_path),
created_at: file_created_at(repository, file_path),
updated_at: file_updated_at(repository, file_path)
}
end

def read_file(file_path, folder \\ "") do
local_path() |> Path.join(folder) |> Path.join(file_path) |> File.read()
end

# Private

defp log(repository, args), do: Git.log!(repository, args)

defp first_in_log(repository, args) do
repository
|> log(args)
|> String.split("\n")
|> List.first()
|> String.trim()
end

defp recursive_ls(path) do
cond do
File.regular?(path) ->
[path]

File.dir?(path) ->
path
|> File.ls!()
|> Enum.map(&Path.join(path, &1))
|> Enum.map(&recursive_ls/1)
|> Enum.concat()

true ->
[]
end
end

defp git_repository do
Logger.info("Cloning repository #{@repository_url}")

case Git.clone(@repository_url) do
{:ok, repo} -> repo
{:error, Git.Error} -> Git.new(@local_path)
end
end

defp file_author(repository, file_name) do
first_in_log(repository, ["--reverse", "--format=%an", file_name])
end

defp file_created_at(repository, file_name) do
case first_in_log(repository, ["--reverse", "--format=%ci", file_name]) do
"" -> DateTime.to_iso8601(DateTime.utc_now())
created_at -> created_at
end
end

defp file_updated_at(repository, file_name) do
case repository |> log(["-1", "--format=%ci", file_name]) |> String.trim() do
"" -> DateTime.to_iso8601(DateTime.utc_now())
updated_at -> updated_at
end
end
end