Commit db0a7e39 by Vicent Martí

Merge pull request #1977 from ethomson/revert

Revert support for a single commit
parents 96fb6a64 bab0b9f2
...@@ -117,6 +117,17 @@ GIT_EXTERN(const char *) git_commit_message(const git_commit *commit); ...@@ -117,6 +117,17 @@ GIT_EXTERN(const char *) git_commit_message(const git_commit *commit);
GIT_EXTERN(const char *) git_commit_message_raw(const git_commit *commit); GIT_EXTERN(const char *) git_commit_message_raw(const git_commit *commit);
/** /**
* Get the short "summary" of the git commit message.
*
* The returned message is the summary of the commit, comprising the
* first paragraph of the message with whitespace trimmed and squashed.
*
* @param commit a previously loaded commit.
* @return the summary of a commit or NULL on error
*/
GIT_EXTERN(const char *) git_commit_summary(git_commit *commit);
/**
* Get the commit time (i.e. committer time) of a commit. * Get the commit time (i.e. committer time) of a commit.
* *
* @param commit a previously loaded commit. * @param commit a previously loaded commit.
......
...@@ -71,6 +71,7 @@ typedef enum { ...@@ -71,6 +71,7 @@ typedef enum {
GITERR_MERGE, GITERR_MERGE,
GITERR_SSH, GITERR_SSH,
GITERR_FILTER, GITERR_FILTER,
GITERR_REVERT,
} git_error_t; } git_error_t;
/** /**
......
...@@ -488,13 +488,13 @@ GIT_EXTERN(int) git_repository_message(char *out, size_t len, git_repository *re ...@@ -488,13 +488,13 @@ GIT_EXTERN(int) git_repository_message(char *out, size_t len, git_repository *re
GIT_EXTERN(int) git_repository_message_remove(git_repository *repo); GIT_EXTERN(int) git_repository_message_remove(git_repository *repo);
/** /**
* Remove all the metadata associated with an ongoing git merge, including * Remove all the metadata associated with an ongoing command like merge,
* MERGE_HEAD, MERGE_MSG, etc. * revert, cherry-pick, etc. For example: MERGE_HEAD, MERGE_MSG, etc.
* *
* @param repo A repository object * @param repo A repository object
* @return 0 on success, or error * @return 0 on success, or error
*/ */
GIT_EXTERN(int) git_repository_merge_cleanup(git_repository *repo); GIT_EXTERN(int) git_repository_state_cleanup(git_repository *repo);
typedef int (*git_repository_fetchhead_foreach_cb)(const char *ref_name, typedef int (*git_repository_fetchhead_foreach_cb)(const char *ref_name,
const char *remote_url, const char *remote_url,
......
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_git_revert_h__
#define INCLUDE_git_revert_h__
#include "common.h"
#include "types.h"
#include "merge.h"
/**
* @file git2/revert.h
* @brief Git revert routines
* @defgroup git_revert Git revert routines
* @ingroup Git
* @{
*/
GIT_BEGIN_DECL
typedef struct {
unsigned int version;
/** For merge commits, the "mainline" is treated as the parent. */
unsigned int mainline;
git_merge_tree_opts merge_tree_opts;
git_checkout_opts checkout_opts;
} git_revert_opts;
#define GIT_REVERT_OPTS_VERSION 1
#define GIT_REVERT_OPTS_INIT {GIT_REVERT_OPTS_VERSION, 0, GIT_MERGE_TREE_OPTS_INIT, GIT_CHECKOUT_OPTS_INIT}
/**
* Reverts the given commits, producing changes in the working directory.
*
* @param repo the repository to revert
* @param commits the commits to revert
* @param commits_len the number of commits to revert
* @param flags merge flags
*/
GIT_EXTERN(int) git_revert(
git_repository *repo,
git_commit *commit,
const git_revert_opts *given_opts);
/** @} */
GIT_END_DECL
#endif
...@@ -31,6 +31,7 @@ void git_commit__free(void *_commit) ...@@ -31,6 +31,7 @@ void git_commit__free(void *_commit)
git__free(commit->raw_header); git__free(commit->raw_header);
git__free(commit->raw_message); git__free(commit->raw_message);
git__free(commit->message_encoding); git__free(commit->message_encoding);
git__free(commit->summary);
git__free(commit); git__free(commit);
} }
...@@ -286,6 +287,34 @@ const char *git_commit_message(const git_commit *commit) ...@@ -286,6 +287,34 @@ const char *git_commit_message(const git_commit *commit)
return message; return message;
} }
const char *git_commit_summary(git_commit *commit)
{
git_buf summary = GIT_BUF_INIT;
const char *msg, *space;
assert(commit);
if (!commit->summary) {
for (msg = git_commit_message(commit), space = NULL; *msg; ++msg) {
if (msg[0] == '\n' && (!msg[1] || msg[1] == '\n'))
break;
else if (msg[0] == '\n')
git_buf_putc(&summary, ' ');
else if (git__isspace(msg[0]))
space = space ? space : msg;
else if (space) {
git_buf_put(&summary, space, (msg - space) + 1);
space = NULL;
} else
git_buf_putc(&summary, *msg);
}
commit->summary = git_buf_detach(&summary);
}
return commit->summary;
}
int git_commit_tree(git_tree **tree_out, const git_commit *commit) int git_commit_tree(git_tree **tree_out, const git_commit *commit)
{ {
assert(commit); assert(commit);
......
...@@ -26,6 +26,8 @@ struct git_commit { ...@@ -26,6 +26,8 @@ struct git_commit {
char *message_encoding; char *message_encoding;
char *raw_message; char *raw_message;
char *raw_header; char *raw_header;
char *summary;
}; };
void git_commit__free(void *commit); void git_commit__free(void *commit);
......
...@@ -1206,7 +1206,7 @@ static git_merge_diff *merge_diff_from_index_entries( ...@@ -1206,7 +1206,7 @@ static git_merge_diff *merge_diff_from_index_entries(
/* Merge trees */ /* Merge trees */
static int merge_index_insert_conflict( static int merge_diff_list_insert_conflict(
git_merge_diff_list *diff_list, git_merge_diff_list *diff_list,
struct merge_diff_df_data *merge_df_data, struct merge_diff_df_data *merge_df_data,
const git_index_entry *tree_items[3]) const git_index_entry *tree_items[3])
...@@ -1222,7 +1222,7 @@ static int merge_index_insert_conflict( ...@@ -1222,7 +1222,7 @@ static int merge_index_insert_conflict(
return 0; return 0;
} }
static int merge_index_insert_unmodified( static int merge_diff_list_insert_unmodified(
git_merge_diff_list *diff_list, git_merge_diff_list *diff_list,
const git_index_entry *tree_items[3]) const git_index_entry *tree_items[3])
{ {
...@@ -1252,7 +1252,7 @@ int git_merge_diff_list__find_differences( ...@@ -1252,7 +1252,7 @@ int git_merge_diff_list__find_differences(
size_t i, j; size_t i, j;
int error = 0; int error = 0;
assert(diff_list && our_tree && their_tree); assert(diff_list && (our_tree || their_tree));
if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
(error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || (error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
...@@ -1262,6 +1262,7 @@ int git_merge_diff_list__find_differences( ...@@ -1262,6 +1262,7 @@ int git_merge_diff_list__find_differences(
/* Set up the iterators */ /* Set up the iterators */
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
error = git_iterator_current(&items[i], iterators[i]); error = git_iterator_current(&items[i], iterators[i]);
if (error < 0 && error != GIT_ITEROVER) if (error < 0 && error != GIT_ITEROVER)
goto done; goto done;
} }
...@@ -1313,9 +1314,9 @@ int git_merge_diff_list__find_differences( ...@@ -1313,9 +1314,9 @@ int git_merge_diff_list__find_differences(
break; break;
if (cur_item_modified) if (cur_item_modified)
error = merge_index_insert_conflict(diff_list, &df_data, cur_items); error = merge_diff_list_insert_conflict(diff_list, &df_data, cur_items);
else else
error = merge_index_insert_unmodified(diff_list, cur_items); error = merge_diff_list_insert_unmodified(diff_list, cur_items);
if (error < 0) if (error < 0)
goto done; goto done;
...@@ -1325,6 +1326,7 @@ int git_merge_diff_list__find_differences( ...@@ -1325,6 +1326,7 @@ int git_merge_diff_list__find_differences(
continue; continue;
error = git_iterator_advance(&items[i], iterators[i]); error = git_iterator_advance(&items[i], iterators[i]);
if (error < 0 && error != GIT_ITEROVER) if (error < 0 && error != GIT_ITEROVER)
goto done; goto done;
} }
...@@ -1569,7 +1571,7 @@ int git_merge_trees( ...@@ -1569,7 +1571,7 @@ int git_merge_trees(
size_t i; size_t i;
int error = 0; int error = 0;
assert(out && repo && our_tree && their_tree); assert(out && repo && (our_tree || their_tree));
*out = NULL; *out = NULL;
...@@ -2268,7 +2270,7 @@ done: ...@@ -2268,7 +2270,7 @@ done:
return error; return error;
} }
static int merge_indexes(git_repository *repo, git_index *index_new) int git_merge__indexes(git_repository *repo, git_index *index_new)
{ {
git_index *index_repo; git_index *index_repo;
unsigned int index_repo_caps; unsigned int index_repo_caps;
...@@ -2360,6 +2362,17 @@ done: ...@@ -2360,6 +2362,17 @@ done:
return error; return error;
} }
static int merge_state_cleanup(git_repository *repo)
{
const char *state_files[] = {
GIT_MERGE_HEAD_FILE,
GIT_MERGE_MODE_FILE,
GIT_MERGE_MSG_FILE,
};
return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files));
}
int git_merge( int git_merge(
git_merge_result **out, git_merge_result **out,
git_repository *repo, git_repository *repo,
...@@ -2440,7 +2453,7 @@ int git_merge( ...@@ -2440,7 +2453,7 @@ int git_merge(
/* TODO: recursive, octopus, etc... */ /* TODO: recursive, octopus, etc... */
if ((error = git_merge_trees(&index_new, repo, ancestor_tree, our_tree, their_trees[0], &opts.merge_tree_opts)) < 0 || if ((error = git_merge_trees(&index_new, repo, ancestor_tree, our_tree, their_trees[0], &opts.merge_tree_opts)) < 0 ||
(error = merge_indexes(repo, index_new)) < 0 || (error = git_merge__indexes(repo, index_new)) < 0 ||
(error = git_repository_index(&index_repo, repo)) < 0 || (error = git_repository_index(&index_repo, repo)) < 0 ||
(error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0) (error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0)
goto on_error; goto on_error;
...@@ -2451,7 +2464,7 @@ int git_merge( ...@@ -2451,7 +2464,7 @@ int git_merge(
goto done; goto done;
on_error: on_error:
git_repository_merge_cleanup(repo); merge_state_cleanup(repo);
git_index_free(index_new); git_index_free(index_new);
git__free(result); git__free(result);
...@@ -2495,39 +2508,6 @@ int git_merge__setup( ...@@ -2495,39 +2508,6 @@ int git_merge__setup(
return error; return error;
} }
int git_repository_merge_cleanup(git_repository *repo)
{
int error = 0;
git_buf merge_head_path = GIT_BUF_INIT,
merge_mode_path = GIT_BUF_INIT,
merge_msg_path = GIT_BUF_INIT;
assert(repo);
if (git_buf_joinpath(&merge_head_path, repo->path_repository, GIT_MERGE_HEAD_FILE) < 0 ||
git_buf_joinpath(&merge_mode_path, repo->path_repository, GIT_MERGE_MODE_FILE) < 0 ||
git_buf_joinpath(&merge_msg_path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0)
return -1;
if (git_path_isfile(merge_head_path.ptr)) {
if ((error = p_unlink(merge_head_path.ptr)) < 0)
goto cleanup;
}
if (git_path_isfile(merge_mode_path.ptr))
(void)p_unlink(merge_mode_path.ptr);
if (git_path_isfile(merge_msg_path.ptr))
(void)p_unlink(merge_msg_path.ptr);
cleanup:
git_buf_free(&merge_msg_path);
git_buf_free(&merge_mode_path);
git_buf_free(&merge_head_path);
return error;
}
/* Merge result data */ /* Merge result data */
int git_merge_result_is_uptodate(git_merge_result *merge_result) int git_merge_result_is_uptodate(git_merge_result *merge_result)
......
...@@ -158,4 +158,6 @@ int git_merge__setup( ...@@ -158,4 +158,6 @@ int git_merge__setup(
size_t their_heads_len, size_t their_heads_len,
unsigned int flags); unsigned int flags);
int git_merge__indexes(git_repository *repo, git_index *index_new);
#endif #endif
...@@ -1965,6 +1965,42 @@ int git_repository_state(git_repository *repo) ...@@ -1965,6 +1965,42 @@ int git_repository_state(git_repository *repo)
return state; return state;
} }
int git_repository__cleanup_files(git_repository *repo, const char *files[], size_t files_len)
{
git_buf path = GIT_BUF_INIT;
size_t i;
int error = 0;
for (i = 0; i < files_len; ++i) {
git_buf_clear(&path);
if ((error = git_buf_joinpath(&path, repo->path_repository, files[i])) < 0 ||
(git_path_isfile(git_buf_cstr(&path)) &&
(error = p_unlink(git_buf_cstr(&path))) < 0))
goto done;
}
done:
git_buf_free(&path);
return error;
}
static const char *state_files[] = {
GIT_MERGE_HEAD_FILE,
GIT_MERGE_MODE_FILE,
GIT_MERGE_MSG_FILE,
GIT_REVERT_HEAD_FILE,
GIT_CHERRY_PICK_HEAD_FILE,
};
int git_repository_state_cleanup(git_repository *repo)
{
assert(repo);
return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files));
}
int git_repository_is_shallow(git_repository *repo) int git_repository_is_shallow(git_repository *repo)
{ {
git_buf path = GIT_BUF_INIT; git_buf path = GIT_BUF_INIT;
......
...@@ -169,4 +169,6 @@ GIT_INLINE(int) git_repository__ensure_not_bare( ...@@ -169,4 +169,6 @@ GIT_INLINE(int) git_repository__ensure_not_bare(
return GIT_EBAREREPO; return GIT_EBAREREPO;
} }
int git_repository__cleanup_files(git_repository *repo, const char *files[], size_t files_len);
#endif #endif
...@@ -149,7 +149,7 @@ int git_reset( ...@@ -149,7 +149,7 @@ int git_reset(
(error = git_index_write(index)) < 0) (error = git_index_write(index)) < 0)
goto cleanup; goto cleanup;
if ((error = git_repository_merge_cleanup(repo)) < 0) { if ((error = git_repository_state_cleanup(repo)) < 0) {
giterr_set(GITERR_INDEX, "%s - failed to clean up merge data", ERROR_MSG); giterr_set(GITERR_INDEX, "%s - failed to clean up merge data", ERROR_MSG);
goto cleanup; goto cleanup;
} }
......
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "common.h"
#include "repository.h"
#include "filebuf.h"
#include "merge.h"
#include "git2/types.h"
#include "git2/merge.h"
#include "git2/revert.h"
#include "git2/commit.h"
#include "git2/sys/commit.h"
#define GIT_REVERT_FILE_MODE 0666
static int write_revert_head(
git_repository *repo,
const git_commit *commit,
const char *commit_oidstr)
{
git_filebuf file = GIT_FILEBUF_INIT;
git_buf file_path = GIT_BUF_INIT;
int error = 0;
assert(repo && commit);
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_REVERT_HEAD_FILE)) >= 0 &&
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_REVERT_FILE_MODE)) >= 0 &&
(error = git_filebuf_printf(&file, "%s\n", commit_oidstr)) >= 0)
error = git_filebuf_commit(&file);
if (error < 0)
git_filebuf_cleanup(&file);
git_buf_free(&file_path);
return error;
}
static int write_merge_msg(
git_repository *repo,
const git_commit *commit,
const char *commit_oidstr,
const char *commit_msgline)
{
git_filebuf file = GIT_FILEBUF_INIT;
git_buf file_path = GIT_BUF_INIT;
int error = 0;
assert(repo && commit);
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MSG_FILE)) < 0 ||
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_REVERT_FILE_MODE)) < 0 ||
(error = git_filebuf_printf(&file, "Revert \"%s\"\n\nThis reverts commit %s.\n",
commit_msgline, commit_oidstr)) < 0)
goto cleanup;
error = git_filebuf_commit(&file);
cleanup:
if (error < 0)
git_filebuf_cleanup(&file);
git_buf_free(&file_path);
return error;
}
static int revert_normalize_opts(
git_repository *repo,
git_revert_opts *opts,
const git_revert_opts *given,
const char *their_label)
{
int error = 0;
unsigned int default_checkout_strategy = GIT_CHECKOUT_SAFE_CREATE |
GIT_CHECKOUT_ALLOW_CONFLICTS;
GIT_UNUSED(repo);
if (given != NULL)
memcpy(opts, given, sizeof(git_revert_opts));
else {
git_revert_opts default_opts = GIT_REVERT_OPTS_INIT;
memcpy(opts, &default_opts, sizeof(git_revert_opts));
}
if (!opts->checkout_opts.checkout_strategy)
opts->checkout_opts.checkout_strategy = default_checkout_strategy;
if (!opts->checkout_opts.our_label)
opts->checkout_opts.our_label = "HEAD";
if (!opts->checkout_opts.their_label)
opts->checkout_opts.their_label = their_label;
return error;
}
static int revert_state_cleanup(git_repository *repo)
{
const char *state_files[] = { GIT_REVERT_HEAD_FILE, GIT_MERGE_MSG_FILE };
return git_repository__cleanup_files(repo, state_files, ARRAY_SIZE(state_files));
}
int git_revert(
git_repository *repo,
git_commit *commit,
const git_revert_opts *given_opts)
{
git_revert_opts opts;
git_commit *parent_commit = NULL;
git_tree *parent_tree = NULL, *our_tree = NULL, *revert_tree = NULL;
git_index *index_new = NULL, *index_repo = NULL;
char commit_oidstr[GIT_OID_HEXSZ + 1];
const char *commit_msg;
git_buf their_label = GIT_BUF_INIT;
int parent = 0;
int error = 0;
assert(repo && commit);
if ((error = git_repository__ensure_not_bare(repo, "revert")) < 0)
return error;
git_oid_fmt(commit_oidstr, git_commit_id(commit));
commit_oidstr[GIT_OID_HEXSZ] = '\0';
if ((commit_msg = git_commit_summary(commit)) == NULL) {
error = -1;
goto on_error;
}
if ((error = git_buf_printf(&their_label, "parent of %.7s... %s", commit_oidstr, commit_msg)) < 0 ||
(error = revert_normalize_opts(repo, &opts, given_opts, git_buf_cstr(&their_label))) < 0 ||
(error = write_revert_head(repo, commit, commit_oidstr)) < 0 ||
(error = write_merge_msg(repo, commit, commit_oidstr, commit_msg)) < 0 ||
(error = git_repository_head_tree(&our_tree, repo)) < 0 ||
(error = git_commit_tree(&revert_tree, commit)) < 0)
goto on_error;
if (git_commit_parentcount(commit) > 1) {
if (!opts.mainline) {
giterr_set(GITERR_REVERT,
"Mainline branch is not specified but %s is a merge commit",
commit_oidstr);
error = -1;
goto on_error;
}
parent = opts.mainline;
} else {
if (opts.mainline) {
giterr_set(GITERR_REVERT,
"Mainline branch was specified but %s is not a merge",
commit_oidstr);
error = -1;
goto on_error;
}
parent = git_commit_parentcount(commit);
}
if (parent &&
((error = git_commit_parent(&parent_commit, commit, (parent - 1))) < 0 ||
(error = git_commit_tree(&parent_tree, parent_commit)) < 0))
goto on_error;
if ((error = git_merge_trees(&index_new, repo, revert_tree, our_tree, parent_tree, &opts.merge_tree_opts)) < 0 ||
(error = git_merge__indexes(repo, index_new)) < 0 ||
(error = git_repository_index(&index_repo, repo)) < 0 ||
(error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0)
goto on_error;
goto done;
on_error:
revert_state_cleanup(repo);
done:
git_index_free(index_new);
git_index_free(index_repo);
git_tree_free(parent_tree);
git_tree_free(our_tree);
git_tree_free(revert_tree);
git_commit_free(parent_commit);
git_buf_free(&their_label);
return error;
}
#include "clar_libgit2.h" #include "clar_libgit2.h"
#include "commit.h"
#include "git2/commit.h"
static git_repository *_repo; static git_repository *_repo;
...@@ -44,3 +46,30 @@ void test_commit_commit__create_unexisting_update_ref(void) ...@@ -44,3 +46,30 @@ void test_commit_commit__create_unexisting_update_ref(void)
git_signature_free(s); git_signature_free(s);
git_reference_free(ref); git_reference_free(ref);
} }
void assert_commit_summary(const char *expected, const char *given)
{
git_commit *dummy;
cl_assert(dummy = git__calloc(1, sizeof(struct git_commit)));
dummy->raw_message = git__strdup(given);
cl_assert_equal_s(expected, git_commit_summary(dummy));
git_commit__free(dummy);
}
void test_commit_commit__summary(void)
{
assert_commit_summary("One-liner with no trailing newline", "One-liner with no trailing newline");
assert_commit_summary("One-liner with trailing newline", "One-liner with trailing newline\n");
assert_commit_summary("Trimmed leading&trailing newlines", "\n\nTrimmed leading&trailing newlines\n\n");
assert_commit_summary("First paragraph only", "\nFirst paragraph only\n\n(There are more!)");
assert_commit_summary("First paragraph with unwrapped trailing\tlines", "\nFirst paragraph\nwith unwrapped\ntrailing\tlines\n\n(Yes, unwrapped!)");
assert_commit_summary("\tLeading \ttabs", "\tLeading\n\ttabs\n\nis preserved");
assert_commit_summary(" Leading Spaces", " Leading\n Spaces\n\nare preserved");
assert_commit_summary("Trailing tabs\tare removed", "Trailing tabs\tare removed\t\t");
assert_commit_summary("Trailing spaces are removed", "Trailing spaces are removed ");
assert_commit_summary("Trailing tabs", "Trailing tabs\t\n\nare removed");
assert_commit_summary("Trailing spaces", "Trailing spaces \n\nare removed");
}
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
hideDotFiles = dotGitOnly
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
2d440f2b3147d3dc7ad1085813478d6d869d5a4d
5acdc74af27172ec491d213ee36cea7eb9ef2579
13ee9cd5d8e1023c218e0e1ea684ec0c582b5050
52c95c4264245469a0617e289a7d737f156826b4
!File one!
!File one!
File one!
File one
File one
File one
File one
File one
File one
File one
File one!
!File one!
!File one!
!File one!
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File six, actually!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment