diff --git a/include/git2.h b/include/git2.h index b5c693a82..a8a430654 100644 --- a/include/git2.h +++ b/include/git2.h @@ -44,6 +44,7 @@ #include "git2/repository.h" #include "git2/revwalk.h" #include "git2/refs.h" +#include "git2/reflog.h" #include "git2/object.h" #include "git2/blob.h" diff --git a/include/git2/reflog.h b/include/git2/reflog.h new file mode 100644 index 000000000..8a2edca4d --- /dev/null +++ b/include/git2/reflog.h @@ -0,0 +1,129 @@ +/* + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2, + * as published by the Free Software Foundation. + * + * In addition to the permissions in the GNU General Public License, + * the authors give you unlimited permission to link the compiled + * version of this file into combinations with other programs, + * and to distribute those combinations without any restriction + * coming from the use of this file. (The General Public License + * restrictions do apply in other respects; for example, they cover + * modification of the file, and distribution when not linked into + * a combined executable.) + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef INCLUDE_git_reflog_h__ +#define INCLUDE_git_reflog_h__ + +#include "common.h" +#include "types.h" +#include "oid.h" + +/** + * @file git2/reflog.h + * @brief Git reflog management routines + * @defgroup git_reflog Git reflog management routines + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +/** + * Read the reflog for the given reference + * + * The reflog must be freed manually by using + * git_reflog_free(). + * + * @param reflog pointer to reflog + * @param ref reference to read the reflog for + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_reflog_read(git_reflog **reflog, git_reference *ref); + +/** + * Write a new reflog for the given reference + * + * If there is no reflog file for the given + * reference yet, it will be created. + * + * `oid_old` may be NULL in case it's a new reference. + * + * `msg` is optional and can be NULL. + * + * @param ref the changed reference + * @param oid_old the OID the reference was pointing to + * @param committer the signature of the committer + * @param msg the reflog message + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_reflog_write(git_reference *ref, const git_oid *oid_old, const git_signature *committer, const char *msg); + +/** + * Get the number of log entries in a reflog + * + * @param reflog the previously loaded reflog + * @return the number of log entries + */ +GIT_EXTERN(unsigned int) git_reflog_entrycount(git_reflog *reflog); + +/** + * Lookup an entry by its index + * + * @param reflog a previously loaded reflog + * @param idx the position to lookup + * @param the entry; NULL if not found + */ +GIT_EXTERN(const git_reflog_entry *) git_reflog_entry_byindex(git_reflog *reflog, unsigned int idx); + +/** + * Get the old oid + * + * @param entry a reflog entry + * @return the old oid + */ +GIT_EXTERN(char *) git_reflog_entry_oidold(const git_reflog_entry *entry); + +/** + * Get the new oid + * + * @param entry a reflog entry + * @return the new oid at this time + */ +GIT_EXTERN(char *) git_reflog_entry_oidnew(const git_reflog_entry *entry); + +/** + * Get the committer of this entry + * + * @param entry a reflog entry + * @return the committer + */ +GIT_EXTERN(git_signature *) git_reflog_entry_committer(const git_reflog_entry *entry); + +/** + * Get the log msg + * + * @param entry a reflog entry + * @return the log msg + */ +GIT_EXTERN(char *) git_reflog_entry_msg(const git_reflog_entry *entry); + +/** + * Free the reflog + * + * @param reflog reflog to free + */ +GIT_EXTERN(void) git_reflog_free(git_reflog *reflog); + +/** @} */ +GIT_END_DECL +#endif diff --git a/include/git2/types.h b/include/git2/types.h index bc1b8a6c7..41d548234 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -141,6 +141,12 @@ typedef struct git_config git_config; /** Interface to access a configuration file */ typedef struct git_config_file git_config_file; +/** Representation of a reference log entry */ +typedef struct git_reflog_entry git_reflog_entry; + +/** Representation of a reference log */ +typedef struct git_reflog git_reflog; + /** Time in a signature */ typedef struct git_time { git_time_t time; /** time in seconds from epoch */ diff --git a/src/reflog.c b/src/reflog.c new file mode 100644 index 000000000..63c4c5005 --- /dev/null +++ b/src/reflog.c @@ -0,0 +1,282 @@ +/* + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2, + * as published by the Free Software Foundation. + * + * In addition to the permissions in the GNU General Public License, + * the authors give you unlimited permission to link the compiled + * version of this file into combinations with other programs, + * and to distribute those combinations without any restriction + * coming from the use of this file. (The General Public License + * restrictions do apply in other respects; for example, they cover + * modification of the file, and distribution when not linked into + * a combined executable.) + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "reflog.h" +#include "repository.h" +#include "filebuf.h" +#include "signature.h" + +static int reflog_init(git_reflog **reflog, git_reference *ref) +{ + git_reflog *log; + + *reflog = NULL; + + log = git__malloc(sizeof(git_reflog)); + if (log == NULL) + return GIT_ENOMEM; + + memset(log, 0x0, sizeof(git_reflog)); + + log->ref_name = git__strdup(ref->name); + + if (git_vector_init(&log->entries, 0, NULL) < 0) { + free(log->ref_name); + free(log); + return GIT_ENOMEM; + } + + *reflog = log; + + return GIT_SUCCESS; +} + +static int reflog_write(git_repository *repo, const char *ref_name, + const char *oid_old, const char *oid_new, + const git_signature *committer, const char *msg) +{ + int error; + char log_path[GIT_PATH_MAX]; + char *sig = NULL; + git_filebuf log; + + assert(repo && ref_name && oid_old && oid_new && committer); + + git_path_join_n(log_path, 3, repo->path_repository, GIT_REFLOG_DIR, ref_name); + + if (git_futils_exists(log_path)) { + if ((error = git_futils_mkpath2file(log_path)) < GIT_SUCCESS) + return git__rethrow(error, "Failed to write reflog. Cannot create reflog directory"); + + } else if (git_futils_isfile(log_path)) + return git__throw(GIT_ERROR, "Failed to write reflog. `%s` is directory", log_path); + + if ((error = git_filebuf_open(&log, log_path, GIT_FILEBUF_APPEND)) < GIT_SUCCESS) + return git__throw(GIT_ERROR, "Failed to write reflog. Cannot open reflog `%s`", log_path); + + if ((error = git_signature__write(&sig, NULL, committer)) < GIT_SUCCESS) + goto cleanup; + + sig[strlen(sig)-1] = '\0'; /* drop LF */ + + if ((error = git_filebuf_printf(&log, "%s %s %s", oid_old, oid_new, sig)) < GIT_SUCCESS) + goto cleanup; + + if (msg) { + if (strchr(msg, '\n')) { + error = git__throw(GIT_ERROR, "msg must not contain newline"); + goto cleanup; + } + + if ((error = git_filebuf_printf(&log, "\t%s", msg)) < GIT_SUCCESS) + goto cleanup; + } + + error = git_filebuf_printf(&log, "\n"); + +cleanup: + if (error < GIT_SUCCESS) + git_filebuf_cleanup(&log); + else + error = git_filebuf_commit(&log); + + if (sig) + free(sig); + + return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to write reflog"); +} + +static int reflog_parse(git_reflog *log, const char *buf, size_t buf_size) +{ + int error; + const char *ptr; + git_reflog_entry *entry; + +#define seek_forward(_increase) { \ + if (_increase >= buf_size) \ + return git__throw(GIT_ERROR, "Failed to seek forward. Buffer size exceeded"); \ + buf += _increase; \ + buf_size -= _increase; \ +} + + while (buf_size > GIT_REFLOG_SIZE_MIN) { + entry = git__malloc(sizeof(git_reflog_entry)); + if (entry == NULL) + return GIT_ENOMEM; + + entry->oid_old = git__strndup(buf, GIT_OID_HEXSZ); + seek_forward(GIT_OID_HEXSZ+1); + + entry->oid_cur = git__strndup(buf, GIT_OID_HEXSZ); + seek_forward(GIT_OID_HEXSZ+1); + + ptr = buf; + + /* Seek forward to the end of the signature. */ + while (*buf && *buf != '\t' && *buf != '\n') + seek_forward(1); + + entry->committer = git__malloc(sizeof(git_signature)); + if (entry->committer == NULL) + return GIT_ENOMEM; + + if ((error = git_signature__parse(entry->committer, &ptr, buf + buf_size, NULL)) < GIT_SUCCESS) + goto cleanup; + + if (*buf == '\t') { + /* We got a message. Read everything till we reach LF. */ + seek_forward(1); + entry->msg = (char *)buf; + + while (*buf && *buf != '\n') + seek_forward(1); + + entry->msg = git__strndup(entry->msg, buf - entry->msg); + } else + entry->msg = NULL; + + while (*buf && *buf == '\n' && buf_size > 1) + seek_forward(1); + + if ((error = git_vector_insert(&log->entries, entry)) < GIT_SUCCESS) + goto cleanup; + } + +#undef seek_forward + +cleanup: + return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse reflog"); +} + +void git_reflog_free(git_reflog *reflog) +{ + unsigned int i; + git_reflog_entry *entry; + + for (i=0; i < reflog->entries.length; i++) { + entry = git_vector_get(&reflog->entries, i); + + free(entry->oid_old); + free(entry->oid_cur); + + git_signature_free(entry->committer); + + free(entry->msg); + free(entry); + } + + git_vector_free(&reflog->entries); + free(reflog->ref_name); + free(reflog); +} + +int git_reflog_read(git_reflog **reflog, git_reference *ref) +{ + int error; + char log_path[GIT_PATH_MAX]; + git_fbuffer log_file = GIT_FBUFFER_INIT; + git_reflog *log = NULL; + + *reflog = NULL; + + if ((error = reflog_init(&log, ref)) < GIT_SUCCESS) + return git__rethrow(error, "Failed to read reflog. Cannot init reflog"); + + git_path_join_n(log_path, 3, ref->owner->path_repository, GIT_REFLOG_DIR, ref->name); + + if ((error = git_futils_readbuffer(&log_file, log_path)) < GIT_SUCCESS) + return git__rethrow(error, "Failed to read reflog. Cannot read file `%s`", log_path); + + error = reflog_parse(log, log_file.data, log_file.len); + + git_futils_freebuffer(&log_file); + + if (error == GIT_SUCCESS) + *reflog = log; + + return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to read reflog"); +} + +int git_reflog_write(git_reference *ref, const git_oid *oid_old, + const git_signature *committer, const char *msg) +{ + int error; + char old[GIT_OID_HEXSZ+1]; + char new[GIT_OID_HEXSZ+1]; + git_reference *r; + const git_oid *oid; + + if ((error = git_reference_resolve(&r, ref)) < GIT_SUCCESS) + return git__rethrow(error, "Failed to write reflog. Cannot resolve reference `%s`", ref->name); + + oid = git_reference_oid(r); + if (oid == NULL) + return git__throw(GIT_ERROR, "Failed to write reflog. Cannot resolve reference `%s`", r->name); + + git_oid_to_string(new, GIT_OID_HEXSZ+1, oid); + + if (oid_old) + git_oid_to_string(old, GIT_OID_HEXSZ+1, oid_old); + else + snprintf(old, GIT_OID_HEXSZ+1, "%0*d", GIT_OID_HEXSZ, 0); + + return reflog_write(ref->owner, ref->name, old, new, committer, msg); +} + +unsigned int git_reflog_entrycount(git_reflog *reflog) +{ + assert(reflog); + return reflog->entries.length; +} + +const git_reflog_entry * git_reflog_entry_byindex(git_reflog *reflog, unsigned int idx) +{ + assert(reflog); + return git_vector_get(&reflog->entries, idx); +} + +char * git_reflog_entry_oidold(const git_reflog_entry *entry) +{ + assert(entry); + return entry->oid_old; +} + +char * git_reflog_entry_oidnew(const git_reflog_entry *entry) +{ + assert(entry); + return entry->oid_cur; +} + +git_signature * git_reflog_entry_committer(const git_reflog_entry *entry) +{ + assert(entry); + return entry->committer; +} + +char * git_reflog_entry_msg(const git_reflog_entry *entry) +{ + assert(entry); + return entry->msg; +} diff --git a/src/reflog.h b/src/reflog.h new file mode 100644 index 000000000..da352ca8f --- /dev/null +++ b/src/reflog.h @@ -0,0 +1,26 @@ +#ifndef INCLUDE_reflog_h__ +#define INCLUDE_reflog_h__ + +#include "common.h" +#include "git2/reflog.h" +#include "vector.h" + +#define GIT_REFLOG_DIR "logs/" + +#define GIT_REFLOG_SIZE_MIN (2*GIT_OID_HEXSZ+2+17) + +struct git_reflog_entry { + char *oid_old; + char *oid_cur; + + git_signature *committer; + + char *msg; +}; + +struct git_reflog { + char *ref_name; + git_vector entries; +}; + +#endif /* INCLUDE_reflog_h__ */ diff --git a/tests/t10-refs.c b/tests/t10-refs.c index 3310bc72c..e004625bd 100644 --- a/tests/t10-refs.c +++ b/tests/t10-refs.c @@ -27,6 +27,9 @@ #include "repository.h" +#include "git2/reflog.h" +#include "reflog.h" + static const char *loose_tag_ref_name = "refs/tags/e90810b"; static const char *non_existing_tag_ref_name = "refs/tags/i-do-not-exist"; @@ -993,6 +996,61 @@ BEGIN_TEST(list1, "try to list only the symbolic references") git_repository_free(repo); END_TEST +static const char *new_ref = "refs/heads/test-reflog"; + +BEGIN_TEST(reflog0, "write a reflog for a given reference") + git_repository *repo; + git_reference *ref; + git_oid oid; + git_signature *committer; + + git_oid_fromstr(&oid, current_master_tip); + + must_pass(git_repository_open(&repo, REPOSITORY_FOLDER)); + + must_pass(git_reference_create_oid(&ref, repo, new_ref, &oid, 0)); + must_pass(git_reference_lookup(&ref, repo, new_ref)); + + committer = git_signature_now("foo", "foo@bar"); + + must_pass(git_reflog_write(ref, NULL, committer, NULL)); + must_fail(git_reflog_write(ref, NULL, committer, "no\nnewline")); + must_pass(git_reflog_write(ref, &oid, committer, "commit: bla bla")); + + git_repository_free(repo); +END_TEST + +BEGIN_TEST(reflog1, "read a reflog for a given reference") + unsigned int i; + git_repository *repo; + git_reference *ref; + git_reflog *reflog; + git_reflog_entry *GIT_UNUSED(entry); + + must_pass(git_repository_open(&repo, REPOSITORY_FOLDER)); + + must_pass(git_reference_lookup(&ref, repo, new_ref)); + + must_pass(git_reflog_read(&reflog, ref)); + + for (i=0; ientries.length; ++i) { + entry = git_vector_get(&reflog->entries, i); + /* + fprintf(stderr, "\nold: %s\n", entry->oid_old); + fprintf(stderr, "cur: %s\n", entry->oid_cur); + fprintf(stderr, "name: %s\n", entry->committer->name); + fprintf(stderr, "mail: %s\n", entry->committer->email); + if (entry->msg) + fprintf(stderr, "msg: %s\n", entry->msg); + */ + } + + git_reflog_free(reflog); + + must_pass(git_reference_delete(ref)); + git_repository_free(repo); +END_TEST + BEGIN_SUITE(refs) ADD_TEST(readtag0); @@ -1034,6 +1092,10 @@ BEGIN_SUITE(refs) ADD_TEST(rename8); ADD_TEST(delete0); + ADD_TEST(list0); ADD_TEST(list1); + + ADD_TEST(reflog0); + ADD_TEST(reflog1); END_SUITE