/* * Copyright (C) 2009-2011 the libgit2 contributors * * 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 "git2/diff.h" #include "diff.h" #include "xdiff/xdiff.h" #include "blob.h" #include "ignore.h" #include static void file_delta_free(git_diff_delta *delta) { if (!delta) return; if (delta->new_path != delta->path) { git__free((char *)delta->new_path); delta->new_path = NULL; } git__free((char *)delta->path); delta->path = NULL; git__free(delta); } static int file_delta_new__from_one( git_diff_list *diff, git_status_t status, mode_t attr, const git_oid *oid, const char *path) { int error; git_diff_delta *delta = git__calloc(1, sizeof(git_diff_delta)); /* This fn is just for single-sided diffs */ assert(status == GIT_STATUS_ADDED || status == GIT_STATUS_DELETED); if (!delta) return git__rethrow(GIT_ENOMEM, "Could not allocate diff record"); if ((delta->path = git__strdup(path)) == NULL) { git__free(delta); return git__rethrow(GIT_ENOMEM, "Could not allocate diff record path"); } if (diff->opts.flags & GIT_DIFF_REVERSE) status = (status == GIT_STATUS_ADDED) ? GIT_STATUS_DELETED : GIT_STATUS_ADDED; delta->status = status; if (status == GIT_STATUS_ADDED) { delta->new_attr = attr; if (oid != NULL) git_oid_cpy(&delta->new_oid, oid); } else { delta->old_attr = attr; if (oid != NULL) git_oid_cpy(&delta->old_oid, oid); } if ((error = git_vector_insert(&diff->files, delta)) < GIT_SUCCESS) file_delta_free(delta); return error; } static int file_delta_new__from_tree_diff( git_diff_list *diff, const git_tree_diff_data *tdiff) { int error; git_diff_delta *delta = git__calloc(1, sizeof(git_diff_delta)); if (!delta) return git__rethrow(GIT_ENOMEM, "Could not allocate diff record"); if ((diff->opts.flags & GIT_DIFF_REVERSE) == 0) { delta->status = tdiff->status; delta->old_attr = tdiff->old_attr; delta->new_attr = tdiff->new_attr; delta->old_oid = tdiff->old_oid; delta->new_oid = tdiff->new_oid; } else { /* reverse the polarity of the neutron flow */ switch (tdiff->status) { case GIT_STATUS_ADDED: delta->status = GIT_STATUS_DELETED; break; case GIT_STATUS_DELETED: delta->status = GIT_STATUS_ADDED; break; default: delta->status = tdiff->status; } delta->old_attr = tdiff->new_attr; delta->new_attr = tdiff->old_attr; delta->old_oid = tdiff->new_oid; delta->new_oid = tdiff->old_oid; } delta->path = git__strdup(diff->pfx.ptr); if (delta->path == NULL) { git__free(delta); return git__rethrow(GIT_ENOMEM, "Could not allocate diff record path"); } if ((error = git_vector_insert(&diff->files, delta)) < GIT_SUCCESS) file_delta_free(delta); return error; } static int create_diff_for_tree_entry(const char *root, git_tree_entry *entry, void *data) { int error; git_diff_list *diff = data; ssize_t pfx_len = diff->pfx.size; if (S_ISDIR(git_tree_entry_attributes(entry))) return GIT_SUCCESS; /* join pfx, root, and entry->filename into one */ if ((error = git_buf_joinpath(&diff->pfx, diff->pfx.ptr, root)) || (error = git_buf_joinpath( &diff->pfx, diff->pfx.ptr, git_tree_entry_name(entry)))) return error; error = file_delta_new__from_one( diff, diff->status, git_tree_entry_attributes(entry), git_tree_entry_id(entry), diff->pfx.ptr); git_buf_truncate(&diff->pfx, pfx_len); return error; } static int tree_to_tree_diff_cb(const git_tree_diff_data *tdiff, void *data) { int error; git_diff_list *diff = data; ssize_t pfx_len = diff->pfx.size; error = git_buf_joinpath(&diff->pfx, diff->pfx.ptr, tdiff->path); if (error < GIT_SUCCESS) return error; /* there are 4 tree related cases: * - diff tree to tree, which just means we recurse * - tree was deleted * - tree was added * - tree became non-tree or vice versa, which git_tree_diff * will already have converted into two calls: an addition * and a deletion (thank you, git_tree_diff!) * otherwise, this is a blob-to-blob diff */ if (S_ISDIR(tdiff->old_attr) && S_ISDIR(tdiff->new_attr)) { git_tree *old = NULL, *new = NULL; if (!(error = git_tree_lookup(&old, diff->repo, &tdiff->old_oid)) && !(error = git_tree_lookup(&new, diff->repo, &tdiff->new_oid))) error = git_tree_diff(old, new, tree_to_tree_diff_cb, diff); git_tree_free(old); git_tree_free(new); } else if (S_ISDIR(tdiff->old_attr) || S_ISDIR(tdiff->new_attr)) { git_tree *tree = NULL; int added_dir = S_ISDIR(tdiff->new_attr); const git_oid *oid = added_dir ? &tdiff->new_oid : &tdiff->old_oid; diff->status = added_dir ? GIT_STATUS_ADDED : GIT_STATUS_DELETED; if (!(error = git_tree_lookup(&tree, diff->repo, oid))) error = git_tree_walk( tree, create_diff_for_tree_entry, GIT_TREEWALK_POST, diff); git_tree_free(tree); } else error = file_delta_new__from_tree_diff(diff, tdiff); git_buf_truncate(&diff->pfx, pfx_len); return error; } static char *git_diff_src_prefix_default = "a/"; static char *git_diff_dst_prefix_default = "b/"; #define PREFIX_IS_DEFAULT(A) \ ((A) == git_diff_src_prefix_default || (A) == git_diff_dst_prefix_default) static char *copy_prefix(const char *prefix) { size_t len = strlen(prefix); char *str = git__malloc(len + 2); if (str != NULL) { memcpy(str, prefix, len + 1); /* append '/' at end if needed */ if (len > 0 && str[len - 1] != '/') { str[len] = '/'; str[len + 1] = '\0'; } } return str; } static git_diff_list *git_diff_list_alloc( git_repository *repo, const git_diff_options *opts) { git_diff_list *diff = git__calloc(1, sizeof(git_diff_list)); if (diff == NULL) return NULL; diff->repo = repo; git_buf_init(&diff->pfx, 0); if (opts == NULL) return diff; memcpy(&diff->opts, opts, sizeof(git_diff_options)); diff->opts.src_prefix = (opts->src_prefix == NULL) ? git_diff_src_prefix_default : copy_prefix(opts->src_prefix); diff->opts.dst_prefix = (opts->dst_prefix == NULL) ? git_diff_dst_prefix_default : copy_prefix(opts->dst_prefix); if (!diff->opts.src_prefix || !diff->opts.dst_prefix) { git__free(diff); return NULL; } if (diff->opts.flags & GIT_DIFF_REVERSE) { char *swap = diff->opts.src_prefix; diff->opts.src_prefix = diff->opts.dst_prefix; diff->opts.dst_prefix = swap; } /* do something safe with the pathspec strarray */ return diff; } void git_diff_list_free(git_diff_list *diff) { git_diff_delta *delta; unsigned int i; if (!diff) return; git_buf_free(&diff->pfx); git_vector_foreach(&diff->files, i, delta) { file_delta_free(delta); diff->files.contents[i] = NULL; } git_vector_free(&diff->files); if (!PREFIX_IS_DEFAULT(diff->opts.src_prefix)) { git__free(diff->opts.src_prefix); diff->opts.src_prefix = NULL; } if (!PREFIX_IS_DEFAULT(diff->opts.dst_prefix)) { git__free(diff->opts.dst_prefix); diff->opts.dst_prefix = NULL; } git__free(diff); } int git_diff_tree_to_tree( git_repository *repo, const git_diff_options *opts, git_tree *old, git_tree *new, git_diff_list **diff_ptr) { int error; git_diff_list *diff = git_diff_list_alloc(repo, opts); if (!diff) return GIT_ENOMEM; error = git_tree_diff(old, new, tree_to_tree_diff_cb, diff); if (error == GIT_SUCCESS) { git_buf_free(&diff->pfx); /* don't need this anymore */ *diff_ptr = diff; } else git_diff_list_free(diff); return error; } typedef struct { git_diff_list *diff; git_index *index; unsigned int index_pos; git_ignores *ignores; } diff_callback_info; static int add_new_index_deltas( diff_callback_info *info, git_status_t status, const char *stop_path) { int error; git_index_entry *idx_entry = git_index_get(info->index, info->index_pos); while (idx_entry != NULL && (stop_path == NULL || strcmp(idx_entry->path, stop_path) < 0)) { error = file_delta_new__from_one( info->diff, status, idx_entry->mode, &idx_entry->oid, idx_entry->path); if (error < GIT_SUCCESS) return error; idx_entry = git_index_get(info->index, ++info->index_pos); } return GIT_SUCCESS; } static int diff_index_to_tree_cb(const char *root, git_tree_entry *tree_entry, void *data) { int error; diff_callback_info *info = data; git_index_entry *idx_entry; /* TODO: submodule support for GIT_OBJ_COMMITs in tree */ if (git_tree_entry_type(tree_entry) != GIT_OBJ_BLOB) return GIT_SUCCESS; error = git_buf_joinpath(&info->diff->pfx, root, git_tree_entry_name(tree_entry)); if (error < GIT_SUCCESS) return error; /* create add deltas for index entries that are not in the tree */ error = add_new_index_deltas(info, GIT_STATUS_ADDED, info->diff->pfx.ptr); if (error < GIT_SUCCESS) return error; /* create delete delta for tree entries that are not in the index */ idx_entry = git_index_get(info->index, info->index_pos); if (idx_entry == NULL || strcmp(idx_entry->path, info->diff->pfx.ptr) > 0) { return file_delta_new__from_one( info->diff, GIT_STATUS_DELETED, git_tree_entry_attributes(tree_entry), git_tree_entry_id(tree_entry), info->diff->pfx.ptr); } /* create modified delta for non-matching tree & index entries */ info->index_pos++; if (git_oid_cmp(&idx_entry->oid, git_tree_entry_id(tree_entry)) || idx_entry->mode != git_tree_entry_attributes(tree_entry)) { git_tree_diff_data tdiff; tdiff.old_attr = git_tree_entry_attributes(tree_entry); tdiff.new_attr = idx_entry->mode; tdiff.status = GIT_STATUS_MODIFIED; tdiff.path = idx_entry->path; git_oid_cpy(&tdiff.old_oid, git_tree_entry_id(tree_entry)); git_oid_cpy(&tdiff.new_oid, &idx_entry->oid); error = file_delta_new__from_tree_diff(info->diff, &tdiff); } return error; } int git_diff_index_to_tree( git_repository *repo, const git_diff_options *opts, git_tree *old, git_diff_list **diff_ptr) { int error; diff_callback_info info = {0}; if ((info.diff = git_diff_list_alloc(repo, opts)) == NULL) return GIT_ENOMEM; if ((error = git_repository_index(&info.index, repo)) == GIT_SUCCESS) { error = git_tree_walk( old, diff_index_to_tree_cb, GIT_TREEWALK_POST, &info); if (error == GIT_SUCCESS) error = add_new_index_deltas(&info, GIT_STATUS_ADDED, NULL); git_index_free(info.index); } git_buf_free(&info.diff->pfx); if (error != GIT_SUCCESS) git_diff_list_free(info.diff); else *diff_ptr = info.diff; return error; } typedef struct { struct stat st; mode_t mode; char path[GIT_FLEX_ARRAY]; } workdir_entry; #define MODE_PERMS_MASK 0777 /* TODO: need equiv of core git's "trust_executable_bit" flag? */ #define CANONICAL_PERMS(MODE) (((MODE) & 0100) ? 0755 : 0644) #define MODE_TYPE(MODE) ((MODE) & ~MODE_PERMS_MASK) static mode_t canonical_mode(mode_t raw_mode) { if (S_ISREG(raw_mode)) return S_IFREG | CANONICAL_PERMS(raw_mode); else if (S_ISLNK(raw_mode)) return S_IFLNK; else if (S_ISDIR(raw_mode)) return S_IFDIR; else if (S_ISGITLINK(raw_mode)) return S_IFGITLINK; else return 0; } static int diff_workdir_insert(void *data, git_buf *dir) { workdir_entry *wd_entry = git__malloc(sizeof(workdir_entry) + dir->size + 2); if (wd_entry == NULL) return GIT_ENOMEM; if (p_lstat(dir->ptr, &wd_entry->st) < 0) { git__free(wd_entry); return GIT_EOSERR; } git_buf_copy_cstr(wd_entry->path, dir->size + 1, dir); wd_entry->mode = canonical_mode(wd_entry->st.st_mode); /* suffix directories with / to mimic tree/index sort order */ if (S_ISDIR(wd_entry->st.st_mode)) { wd_entry->path[dir->size] = '/'; wd_entry->path[dir->size+1] = '\0'; } return git_vector_insert((git_vector *)data, wd_entry); } static int diff_workdir_walk( const char *dir, diff_callback_info *info, int (*cb)(diff_callback_info *, workdir_entry *)) { int error = GIT_SUCCESS; git_vector files = GIT_VECTOR_INIT; git_buf buf = GIT_BUF_INIT; unsigned int i; workdir_entry *wd_entry; git_ignores ignores = {0}, *old_ignores = info->ignores; if (!dir) dir = git_repository_workdir(info->diff->repo); if ((error = git_vector_init(&files, 0, git__strcmp_cb)) < GIT_SUCCESS || (error = git_buf_sets(&buf, dir)) < GIT_SUCCESS || (error = git_path_direach(&buf, diff_workdir_insert, &files)) < GIT_SUCCESS || (error = git_ignore__for_path(info->diff->repo, dir, &ignores)) < GIT_SUCCESS) goto cleanup; git_vector_sort(&files); info->ignores = old_ignores; git_vector_foreach(&files, i, wd_entry) { if ((error = cb(info, wd_entry)) < GIT_SUCCESS) goto cleanup; } cleanup: git_vector_foreach(&files, i, wd_entry) git__free(wd_entry); info->ignores = old_ignores; git_ignore__free(&ignores); git_vector_free(&files); git_buf_free(&buf); return error; } static int found_new_workdir_entry( diff_callback_info *info, workdir_entry *wd_entry) { int error; int ignored = 0; git_status_t status; /* skip file types that are not trackable */ if (wd_entry->mode == 0) return GIT_SUCCESS; error = git_ignore__lookup(info->ignores, wd_entry->path, &ignored); if (error < GIT_SUCCESS) return error; status = ignored ? GIT_STATUS_IGNORED : GIT_STATUS_UNTRACKED; return file_delta_new__from_one( info->diff, status, wd_entry->mode, NULL, wd_entry->path); } static int diff_workdir_to_index_cb( diff_callback_info *info, workdir_entry *wd_entry) { int error, modified; git_index_entry *idx_entry; git_oid new_oid; /* Store index entries that precede this workdir entry */ error = add_new_index_deltas(info, GIT_STATUS_DELETED, wd_entry->path); if (error < GIT_SUCCESS) return error; /* Process workdir entries that are not in the index. * These might be untracked, ignored, or special (dirs, etc). */ idx_entry = git_index_get(info->index, info->index_pos); if (idx_entry == NULL || strcmp(idx_entry->path, wd_entry->path) > 0) { git_buf dotgit = GIT_BUF_INIT; int contains_dotgit; if (!S_ISDIR(wd_entry->mode)) return found_new_workdir_entry(info, wd_entry); error = git_buf_joinpath(&dotgit, wd_entry->path, DOT_GIT); if (error < GIT_SUCCESS) return error; contains_dotgit = (git_path_exists(dotgit.ptr) == GIT_SUCCESS); git_buf_free(&dotgit); if (contains_dotgit) /* TODO: deal with submodule or embedded repo */ return GIT_SUCCESS; else if (git__prefixcmp(idx_entry->path, wd_entry->path) == GIT_SUCCESS) /* there are entries in the directory in the index already, * so recurse into it. */ return diff_workdir_walk(wd_entry->path, info, diff_workdir_to_index_cb); else /* TODO: this is not the same behavior as core git. * * I don't recurse into the directory once I know that no files * in it are being tracked. But core git does and only adds an * entry if there are non-directory entries contained under the * dir (although, interestingly, it only shows the dir, not the * individual entries). */ return found_new_workdir_entry(info, wd_entry); } /* create modified delta for non-matching tree & index entries */ info->index_pos++; /* check for symlink/blob changes and split into add/del pair */ if (MODE_TYPE(wd_entry->mode) != MODE_TYPE(idx_entry->mode)) { error = file_delta_new__from_one( info->diff, GIT_STATUS_DELETED, idx_entry->mode, &idx_entry->oid, idx_entry->path); if (error < GIT_SUCCESS) return error; /* because of trailing slash, cannot have non-dir to dir transform */ assert(!S_ISDIR(wd_entry->mode)); return file_delta_new__from_one( info->diff, GIT_STATUS_ADDED, wd_entry->mode, NULL, wd_entry->path); } /* mode or size changed, so git blob has definitely changed */ if (wd_entry->mode != idx_entry->mode || wd_entry->st.st_size != idx_entry->file_size) { modified = 1; memset(&new_oid, 0, sizeof(new_oid)); } /* all other things are indicators there might be a change, so get oid */ if (!modified && ((git_time_t)wd_entry->st.st_ctime != idx_entry->ctime.seconds || (git_time_t)wd_entry->st.st_mtime != idx_entry->mtime.seconds || (unsigned int)wd_entry->st.st_dev != idx_entry->dev || (unsigned int)wd_entry->st.st_ino != idx_entry->ino || /* TODO: need TRUST_UID_GID configs */ (unsigned int)wd_entry->st.st_uid != idx_entry->uid || (unsigned int)wd_entry->st.st_gid != idx_entry->gid)) { /* calculate oid to confirm change */ if (S_ISLNK(wd_entry->st.st_mode)) error = git_odb__hashlink(&new_oid, wd_entry->path); else { int fd; if ((fd = p_open(wd_entry->path, O_RDONLY)) < 0) error = git__throw( GIT_EOSERR, "Could not open '%s'", wd_entry->path); else { error = git_odb__hashfd( &new_oid, fd, wd_entry->st.st_size, GIT_OBJ_BLOB); p_close(fd); } } if (error < GIT_SUCCESS) return error; modified = (git_oid_cmp(&new_oid, &idx_entry->oid) != 0); } /* TODO: check index flags for forced ignore changes */ if (modified) { git_tree_diff_data tdiff; tdiff.old_attr = idx_entry->mode; tdiff.new_attr = wd_entry->mode; tdiff.status = GIT_STATUS_MODIFIED; tdiff.path = wd_entry->path; git_oid_cpy(&tdiff.old_oid, &idx_entry->oid); git_oid_cpy(&tdiff.new_oid, &new_oid); error = file_delta_new__from_tree_diff(info->diff, &tdiff); } return error; } int git_diff_workdir_to_index( git_repository *repo, const git_diff_options *opts, git_diff_list **diff) { int error; diff_callback_info info = {0}; if ((info.diff = git_diff_list_alloc(repo, opts)) == NULL) return GIT_ENOMEM; if ((error = git_repository_index(&info.index, repo)) == GIT_SUCCESS) { error = diff_workdir_walk(NULL, &info, diff_workdir_to_index_cb); if (error == GIT_SUCCESS) error = add_new_index_deltas(&info, GIT_STATUS_DELETED, NULL); git_index_free(info.index); } git_buf_free(&info.diff->pfx); if (error != GIT_SUCCESS) git_diff_list_free(info.diff); else *diff = info.diff; return error; } typedef struct { git_diff_list *diff; void *cb_data; git_diff_hunk_fn hunk_cb; git_diff_line_fn line_cb; unsigned int index; git_diff_delta *delta; } diff_output_info; static int read_next_int(const char **str, int *value) { const char *scan = *str; int v = 0, digits = 0; /* find next digit */ for (scan = *str; *scan && !isdigit(*scan); scan++); /* parse next number */ for (; isdigit(*scan); scan++, digits++) v = (v * 10) + (*scan - '0'); *str = scan; *value = v; return (digits > 0) ? GIT_SUCCESS : GIT_ENOTFOUND; } static int diff_output_cb(void *priv, mmbuffer_t *bufs, int len) { int err = GIT_SUCCESS; diff_output_info *info = priv; if (len == 1 && info->hunk_cb) { git_diff_range range = { -1, 0, -1, 0 }; /* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */ if (bufs[0].ptr[0] == '@') { const char *scan = bufs[0].ptr; if (!(err = read_next_int(&scan, &range.old_start)) && *scan == ',') err = read_next_int(&scan, &range.old_lines); if (!err && !(err = read_next_int(&scan, &range.new_start)) && *scan == ',') err = read_next_int(&scan, &range.new_lines); if (!err && range.old_start >= 0 && range.new_start >= 0) err = info->hunk_cb( info->cb_data, info->delta, &range, bufs[0].ptr, bufs[0].size); } } else if ((len == 2 || len == 3) && info->line_cb) { int origin; /* expect " "/"-"/"+", then data, then maybe newline */ origin = (*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION : (*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION : GIT_DIFF_LINE_CONTEXT; err = info->line_cb( info->cb_data, info->delta, origin, bufs[1].ptr, bufs[1].size); /* deal with adding and removing newline at EOF */ if (err == GIT_SUCCESS && len == 3) { if (origin == GIT_DIFF_LINE_ADDITION) origin = GIT_DIFF_LINE_ADD_EOFNL; else origin = GIT_DIFF_LINE_DEL_EOFNL; err = info->line_cb( info->cb_data, info->delta, origin, bufs[2].ptr, bufs[2].size); } } return err; } static int set_file_is_binary( git_repository *repo, git_diff_delta *file, mmfile_t *old, mmfile_t *new) { int error; const char *value; /* check diff attribute +, -, or 0 */ error = git_attr_get(repo, file->path, "diff", &value); if (error != GIT_SUCCESS) return error; if (value == GIT_ATTR_TRUE) { file->binary = 0; return GIT_SUCCESS; } if (value == GIT_ATTR_FALSE) { file->binary = 1; return GIT_SUCCESS; } /* TODO: if value != NULL, implement diff drivers */ /* TODO: check if NUL byte appears in first bit */ GIT_UNUSED_ARG(old); GIT_UNUSED_ARG(new); file->binary = 0; return GIT_SUCCESS; } static void setup_xdiff_options( git_diff_options *opts, xdemitconf_t *cfg, xpparam_t *param) { memset(cfg, 0, sizeof(xdemitconf_t)); memset(param, 0, sizeof(xpparam_t)); cfg->ctxlen = (!opts || !opts->context_lines) ? 3 : opts->context_lines; cfg->interhunkctxlen = (!opts || !opts->interhunk_lines) ? 3 : opts->interhunk_lines; if (!opts) return; if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE) param->flags |= XDF_WHITESPACE_FLAGS; if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE_CHANGE) param->flags |= XDF_IGNORE_WHITESPACE_CHANGE; if (opts->flags & GIT_DIFF_IGNORE_WHITESPACE_EOL) param->flags |= XDF_IGNORE_WHITESPACE_AT_EOL; } int git_diff_foreach( git_diff_list *diff, void *data, git_diff_file_fn file_cb, git_diff_hunk_fn hunk_cb, git_diff_line_fn line_cb) { int error = GIT_SUCCESS; diff_output_info info; git_diff_delta *delta; xpparam_t xdiff_params; xdemitconf_t xdiff_config; xdemitcb_t xdiff_callback; info.diff = diff; info.cb_data = data; info.hunk_cb = hunk_cb; info.line_cb = line_cb; setup_xdiff_options(&diff->opts, &xdiff_config, &xdiff_params); memset(&xdiff_callback, 0, sizeof(xdiff_callback)); xdiff_callback.outf = diff_output_cb; xdiff_callback.priv = &info; git_vector_foreach(&diff->files, info.index, delta) { mmfile_t old_data, new_data; /* map files */ if (hunk_cb || line_cb) { /* TODO: Partial blob reading to defer loading whole blob. * I.e. I want a blob with just the first 4kb loaded, then * later on I will read the rest of the blob if needed. */ if (delta->status == GIT_STATUS_DELETED || delta->status == GIT_STATUS_MODIFIED) { error = git_blob_lookup( &delta->old_blob, diff->repo, &delta->old_oid); old_data.ptr = (char *)git_blob_rawcontent(delta->old_blob); old_data.size = git_blob_rawsize(delta->old_blob); } else { delta->old_blob = NULL; old_data.ptr = ""; old_data.size = 0; } if (delta->status == GIT_STATUS_ADDED || delta->status == GIT_STATUS_MODIFIED) { error = git_blob_lookup( &delta->new_blob, diff->repo, &delta->new_oid); new_data.ptr = (char *)git_blob_rawcontent(delta->new_blob); new_data.size = git_blob_rawsize(delta->new_blob); } else { delta->new_blob = NULL; new_data.ptr = ""; new_data.size = 0; } } if (diff->opts.flags & GIT_DIFF_FORCE_TEXT) delta->binary = 0; else if ((error = set_file_is_binary( diff->repo, delta, &old_data, &new_data)) < GIT_SUCCESS) break; /* TODO: if ignore_whitespace is set, then we *must* do text * diffs to tell if a file has really been changed. */ if (file_cb != NULL) { error = file_cb(data, delta, (float)info.index / diff->files.length); if (error != GIT_SUCCESS) break; } /* don't do hunk and line diffs if file is binary */ if (delta->binary) continue; /* nothing to do if we did not get a blob */ if (!delta->old_blob && !delta->new_blob) continue; assert(hunk_cb || line_cb); info.delta = delta; xdl_diff(&old_data, &new_data, &xdiff_params, &xdiff_config, &xdiff_callback); git_blob_free(delta->old_blob); delta->old_blob = NULL; git_blob_free(delta->new_blob); delta->new_blob = NULL; } return error; } typedef struct { git_diff_list *diff; git_diff_output_fn print_cb; void *cb_data; git_buf *buf; } diff_print_info; static char pick_suffix(int mode) { if (S_ISDIR(mode)) return '/'; else if (mode & 0100) /* modes in git are not very flexible, so if this bit is set, * we must be dealwith with a 100755 type of file. */ return '*'; else return ' '; } static int print_compact(void *data, git_diff_delta *delta, float progress) { diff_print_info *pi = data; char code, old_suffix, new_suffix; GIT_UNUSED_ARG(progress); switch (delta->status) { case GIT_STATUS_ADDED: code = 'A'; break; case GIT_STATUS_DELETED: code = 'D'; break; case GIT_STATUS_MODIFIED: code = 'M'; break; case GIT_STATUS_RENAMED: code = 'R'; break; case GIT_STATUS_COPIED: code = 'C'; break; case GIT_STATUS_IGNORED: code = 'I'; break; case GIT_STATUS_UNTRACKED: code = '?'; break; default: code = 0; } if (!code) return GIT_SUCCESS; old_suffix = pick_suffix(delta->old_attr); new_suffix = pick_suffix(delta->new_attr); git_buf_clear(pi->buf); if (delta->new_path != NULL) git_buf_printf(pi->buf, "%c\t%s%c -> %s%c\n", code, delta->path, old_suffix, delta->new_path, new_suffix); else if (delta->old_attr != delta->new_attr && delta->old_attr != 0 && delta->new_attr != 0) git_buf_printf(pi->buf, "%c\t%s%c (%o -> %o)\n", code, delta->path, new_suffix, delta->old_attr, delta->new_attr); else if (old_suffix != ' ') git_buf_printf(pi->buf, "%c\t%s%c\n", code, delta->path, old_suffix); else git_buf_printf(pi->buf, "%c\t%s\n", code, delta->path); if (git_buf_lasterror(pi->buf) != GIT_SUCCESS) return git_buf_lasterror(pi->buf); return pi->print_cb(pi->cb_data, GIT_DIFF_LINE_FILE_HDR, pi->buf->ptr); } int git_diff_print_compact( git_diff_list *diff, void *cb_data, git_diff_output_fn print_cb) { int error; git_buf buf = GIT_BUF_INIT; diff_print_info pi; pi.diff = diff; pi.print_cb = print_cb; pi.cb_data = cb_data; pi.buf = &buf; error = git_diff_foreach(diff, &pi, print_compact, NULL, NULL); git_buf_free(&buf); return error; } static int print_oid_range(diff_print_info *pi, git_diff_delta *delta) { char start_oid[8], end_oid[8]; /* TODO: Determine a good actual OID range to print */ git_oid_to_string(start_oid, sizeof(start_oid), &delta->old_oid); git_oid_to_string(end_oid, sizeof(end_oid), &delta->new_oid); /* TODO: Match git diff more closely */ if (delta->old_attr == delta->new_attr) { git_buf_printf(pi->buf, "index %s..%s %o\n", start_oid, end_oid, delta->old_attr); } else { if (delta->old_attr == 0) { git_buf_printf(pi->buf, "new file mode %o\n", delta->new_attr); } else if (delta->new_attr == 0) { git_buf_printf(pi->buf, "deleted file mode %o\n", delta->old_attr); } else { git_buf_printf(pi->buf, "old mode %o\n", delta->old_attr); git_buf_printf(pi->buf, "new mode %o\n", delta->new_attr); } git_buf_printf(pi->buf, "index %s..%s\n", start_oid, end_oid); } return git_buf_lasterror(pi->buf); } static int print_patch_file(void *data, git_diff_delta *delta, float progress) { int error; diff_print_info *pi = data; const char *oldpfx = pi->diff->opts.src_prefix; const char *oldpath = delta->path; const char *newpfx = pi->diff->opts.dst_prefix; const char *newpath = delta->new_path ? delta->new_path : delta->path; GIT_UNUSED_ARG(progress); git_buf_clear(pi->buf); git_buf_printf(pi->buf, "diff --git %s%s %s%s\n", oldpfx, delta->path, newpfx, newpath); if ((error = print_oid_range(pi, delta)) < GIT_SUCCESS) return error; if (delta->old_blob == NULL) { oldpfx = ""; oldpath = "/dev/null"; } if (delta->new_blob == NULL) { oldpfx = ""; oldpath = "/dev/null"; } if (!delta->binary) { git_buf_printf(pi->buf, "--- %s%s\n", oldpfx, oldpath); git_buf_printf(pi->buf, "+++ %s%s\n", newpfx, newpath); } if (git_buf_lasterror(pi->buf) != GIT_SUCCESS) return git_buf_lasterror(pi->buf); error = pi->print_cb(pi->cb_data, GIT_DIFF_LINE_FILE_HDR, pi->buf->ptr); if (error != GIT_SUCCESS || !delta->binary) return error; git_buf_clear(pi->buf); git_buf_printf( pi->buf, "Binary files %s%s and %s%s differ\n", oldpfx, oldpath, newpfx, newpath); if (git_buf_lasterror(pi->buf) != GIT_SUCCESS) return git_buf_lasterror(pi->buf); return pi->print_cb(pi->cb_data, GIT_DIFF_LINE_BINARY, pi->buf->ptr); } static int print_patch_hunk( void *data, git_diff_delta *d, git_diff_range *r, const char *header, size_t header_len) { diff_print_info *pi = data; GIT_UNUSED_ARG(d); GIT_UNUSED_ARG(r); git_buf_clear(pi->buf); if (git_buf_printf(pi->buf, "%.*s", (int)header_len, header) == GIT_SUCCESS) return pi->print_cb(pi->cb_data, GIT_DIFF_LINE_HUNK_HDR, pi->buf->ptr); else return git_buf_lasterror(pi->buf); } static int print_patch_line( void *data, git_diff_delta *delta, char line_origin, /* GIT_DIFF_LINE value from above */ const char *content, size_t content_len) { diff_print_info *pi = data; GIT_UNUSED_ARG(delta); git_buf_clear(pi->buf); if (line_origin == GIT_DIFF_LINE_ADDITION || line_origin == GIT_DIFF_LINE_DELETION || line_origin == GIT_DIFF_LINE_CONTEXT) git_buf_printf(pi->buf, "%c%.*s", line_origin, (int)content_len, content); else if (content_len > 0) git_buf_printf(pi->buf, "%.*s", (int)content_len, content); if (git_buf_lasterror(pi->buf) != GIT_SUCCESS) return git_buf_lasterror(pi->buf); return pi->print_cb(pi->cb_data, line_origin, pi->buf->ptr); } int git_diff_print_patch( git_diff_list *diff, void *cb_data, git_diff_output_fn print_cb) { int error; git_buf buf = GIT_BUF_INIT; diff_print_info pi; pi.diff = diff; pi.print_cb = print_cb; pi.cb_data = cb_data; pi.buf = &buf; error = git_diff_foreach( diff, &pi, print_patch_file, print_patch_hunk, print_patch_line); git_buf_free(&buf); return error; } int git_diff_blobs( git_repository *repo, git_blob *old_blob, git_blob *new_blob, git_diff_options *options, void *cb_data, git_diff_hunk_fn hunk_cb, git_diff_line_fn line_cb) { diff_output_info info; git_diff_delta delta; mmfile_t old, new; xpparam_t xdiff_params; xdemitconf_t xdiff_config; xdemitcb_t xdiff_callback; assert(repo); if (options && (options->flags & GIT_DIFF_REVERSE)) { git_blob *swap = old_blob; old_blob = new_blob; new_blob = swap; } if (old_blob) { old.ptr = (char *)git_blob_rawcontent(old_blob); old.size = git_blob_rawsize(old_blob); } else { old.ptr = ""; old.size = 0; } if (new_blob) { new.ptr = (char *)git_blob_rawcontent(new_blob); new.size = git_blob_rawsize(new_blob); } else { new.ptr = ""; new.size = 0; } /* populate a "fake" delta record */ delta.status = old.ptr ? (new.ptr ? GIT_STATUS_MODIFIED : GIT_STATUS_DELETED) : (new.ptr ? GIT_STATUS_ADDED : GIT_STATUS_UNTRACKED); delta.old_attr = 0100644; /* can't know the truth from a blob alone */ delta.new_attr = 0100644; git_oid_cpy(&delta.old_oid, git_object_id((const git_object *)old_blob)); git_oid_cpy(&delta.new_oid, git_object_id((const git_object *)new_blob)); delta.old_blob = old_blob; delta.new_blob = new_blob; delta.path = NULL; delta.new_path = NULL; delta.similarity = 0; delta.binary = 0; info.diff = NULL; info.delta = δ info.cb_data = cb_data; info.hunk_cb = hunk_cb; info.line_cb = line_cb; setup_xdiff_options(options, &xdiff_config, &xdiff_params); memset(&xdiff_callback, 0, sizeof(xdiff_callback)); xdiff_callback.outf = diff_output_cb; xdiff_callback.priv = &info; xdl_diff(&old, &new, &xdiff_params, &xdiff_config, &xdiff_callback); return GIT_SUCCESS; }