Skip to content

Commit

Permalink
[wip] attach: introduce add subcommand
Browse files Browse the repository at this point in the history
The `git attach` built-in command is used to create, update, delete,
view, download attachments on a commit, or other operations that may be
supported in the future.

This commit only take the first step. We introduced a new builtin `git
attach` before, then bring the `add` subcommand to parse the given
regular files into ATTACH_BLOBs. We then construct an ATTACH_TREE which
groups these ATTACH_BLOBs together. Next step, a ATTACH_COMMIT is built
to referenece to the ATTACH_TREE. At last, we associate the
ATTACH_REFERENCE(`refs/attachments/commits` by default) to the
ATTACH_COMMIT.

An attachment refers to an regular file. if the specified pathspec is
a dir, or a multi-level dir, without any regular files in it, we will
give warnings and do nothing with ATTACH_REFERENCE.

Signed-off-by: Teng Long <[email protected]>
  • Loading branch information
dyrone committed Nov 30, 2023
1 parent ea33355 commit 8487565
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 1 deletion.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ LIB_OBJS += apply.o
LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += attach.o
LIB_OBJS += attr.o
LIB_OBJS += base85.o
LIB_OBJS += bisect.o
Expand Down
15 changes: 15 additions & 0 deletions attach.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include "git-compat-util.h"
#include "environment.h"
#include "attach.h"

const char *default_attachments_ref(void)
{
const char *attachments_ref = NULL;
if (!attachments_ref)
attachments_ref = getenv(GIT_ATTACHMENTS_REF_ENVIRONMENT);
if (!attachments_ref)
attachments_ref = attachments_ref_name;
if (!attachments_ref)
attachments_ref = GIT_ATTACHMENTS_DEFAULT_REF;
return attachments_ref;
}
15 changes: 15 additions & 0 deletions attach.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef ATTACH_H
#define ATTACH_H

/*
* Return the default attachments ref.
*
* This the first of the following to be defined:
* 1. The '--ref' option to 'git attach', if given
* 2. The $GIT_ATTACHMENTS_REF environment variable, if set
* 3. The value of the core.attachmentsRef config variable, if set
* 4. GIT_NOTES_DEFAULT_REF (i.e. "refs/attachments/commits")
*/
const char *default_attachments_ref(void);

#endif
182 changes: 181 additions & 1 deletion builtin/attach.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,187 @@
#include "builtin.h"
#include "gettext.h"
#include "parse-options.h"
#include "config.h"
#include "pathspec.h"
#include "setup.h"
#include "object-store.h"
#include "object-file.h"
#include "object-name.h"
#include "hex.h"
#include "attach.h"
#include "refs.h"
#include "tree-walk.h"
#include "tree.h"

static const char * const git_attach_usage[] = {
N_("git attach add [--commitish] <file>"),
NULL
};

static const char *const git_attach_add_usage[] = {
N_("git attach add [--commitish] <file>"),
NULL
};

static int write_attach_ref(const char *ref, struct object_id *attach_tree,
const char *msg)
{
const char *attachments_ref;
struct object_id commit_oid;
struct object_id parent_oid;
struct tree_desc desc;
struct name_entry entry;
struct tree *tree = NULL;
struct commit_list *parents = NULL;
struct strbuf buf = STRBUF_INIT;

if (!ref)
attachments_ref = default_attachments_ref();

if (!read_ref(attachments_ref, &parent_oid)) {
struct commit *parent = lookup_commit(the_repository, &parent_oid);
if (repo_parse_commit(the_repository, parent))
die("Failed to find/parse commit %s", attachments_ref);
commit_list_insert(parent, &parents);
}

tree = repo_get_commit_tree(the_repository,
lookup_commit(the_repository, &parent_oid));
init_tree_desc(&desc, tree->buffer, tree->size);

while (tree_entry(&desc, &entry)) {
switch (object_type(entry.mode)) {
case OBJ_TREE:
fprintf(stderr, "entry.path: %s\n", entry.path);
continue;
default:
continue;
}
}

if (commit_tree(msg, strlen(msg), attach_tree, parents, &commit_oid,
NULL, NULL))
die(_("Failed to commit attachment tree to database"));

strbuf_addstr(&buf, msg);
update_ref(buf.buf, attachments_ref, &commit_oid, NULL, 0,
UPDATE_REFS_DIE_ON_ERR);

strbuf_release(&buf);
return 0;
}

/*
* * Returns 0 on success, -1 if no attachments, 1 on failure
*/
static int write_attach_tree(const struct pathspec *pathspec,
struct object_id *attach_tree,
struct object_id *attach_commit)
{
struct strbuf file_buf = STRBUF_INIT;
struct strbuf attach_tree_buf = STRBUF_INIT;
struct dir_struct dir = DIR_INIT;
struct object_id oid;
struct stat st;
unsigned flags = HASH_FORMAT_CHECK | HASH_WRITE_OBJECT;

dir.flags |= DIR_SHOW_OTHER_DIRECTORIES | DIR_SHOW_IGNORED_TOO |
DIR_KEEP_UNTRACKED_CONTENTS | DIR_HIDE_EMPTY_DIRECTORIES;
fill_directory(&dir, the_repository->index, pathspec);

// 有没有现成的API解决这个问题, 将dir转换为tree?
// 没找到,自己写吧
for (int i = 0; i < dir.nr; i++) {
strbuf_reset(&file_buf);

if (lstat(dir.entries[i]->name, &st))
die_errno(_("fail to stat file '%s'"),
dir.entries[i]->name);

if (!(S_ISREG(st.st_mode))) {
warning(_("attachment is not a regular file, skipped '%s'"),
dir.entries[i]->name);
continue;
}

if (strbuf_read_file(&file_buf, dir.entries[i]->name, 0) < 0)
die(_("unable to read filepath '%s'"),
dir.entries[i]->name);

if (write_object_file_flags(file_buf.buf, file_buf.len,
OBJ_BLOB, &oid, flags))
die(_("unable to add blob from %s to database"),
dir.entries[i]->name);

strbuf_addf(&attach_tree_buf, "%o %s%c", 0100644,
dir.entries[i]->name, '\0');
strbuf_add(&attach_tree_buf, oid.hash, the_hash_algo->rawsz);
}

if (attach_tree_buf.len <= 0)
die(_("no attachments found in given pathspec"));
else
write_object_file(attach_tree_buf.buf, attach_tree_buf.len,
OBJ_TREE, attach_tree);

strbuf_release(&file_buf);
strbuf_release(&attach_tree_buf);
return 0;
}

static int add(int argc, const char **argv, const char *prefix)
{
struct pathspec pathspec;
struct object_id attach_tree;
struct object_id attach_commit;
char *attach_commitish = NULL;
char *attachments_msg = "Attachments updated by 'git attach add'";

struct option options[] = {
OPT_STRING(0, "commit", &attach_commitish, N_("commit"),
N_("the commit which the attachments reference to")),
OPT_END()
};

parse_options(argc, argv, prefix, options, git_attach_add_usage,
PARSE_OPT_KEEP_ARGV0);
attach_commitish = attach_commitish ? attach_commitish : "HEAD";

if (repo_get_oid_commit(the_repository, attach_commitish,
&attach_commit))
die(_("unable to find commit %s"), attach_commitish);

parse_pathspec(&pathspec, 0,
PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH,
prefix, argv + 1);
if (!pathspec.nr)
die(_("nothing specified, nothing to attach"));

if (write_attach_tree(&pathspec, &attach_tree, &attach_commit))
die(_("unable to write attach tree object"));

if (write_attach_ref(NULL, &attach_tree, attachments_msg))
die(_("unable to write attach ref"));

clear_pathspec(&pathspec);
return 0;
}

int cmd_attach(int argc, const char **argv, const char *prefix)
{
return error(_("subcommand `%s' not implement yet"), argv[0]);
parse_opt_subcommand_fn *fn = NULL;
struct option options[] = { OPT_SUBCOMMAND("add", &fn, add),
OPT_END() };

git_config(git_default_config, NULL);

argc = parse_options(argc, argv, prefix, options, git_attach_usage,
PARSE_OPT_SUBCOMMAND_OPTIONAL);

if (!fn) {
error(_("subcommand `%s' not implement yet"), argv[0]);
usage_with_options(git_attach_usage, options);
}

return !!fn(argc, argv, prefix);
}
5 changes: 5 additions & 0 deletions config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,11 @@ static int git_default_branch_config(const char *var, const char *value)
return 0;
}

if (!strcmp(var, "core.attachmentsref")) {
attachments_ref_name = xstrdup(value);
return 0;
}

/* Add other config variables here and to Documentation/config.txt. */
return 0;
}
Expand Down
1 change: 1 addition & 0 deletions environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
#endif
enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
char *notes_ref_name;
char *attachments_ref_name;
int grafts_keep_true_parents;
int core_apply_sparse_checkout;
int core_sparse_checkout_cone;
Expand Down
4 changes: 4 additions & 0 deletions environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const char *getenv_safe(struct strvec *argv, const char *name);
#define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS"
#define GIT_TEXT_DOMAIN_DIR_ENVIRONMENT "GIT_TEXTDOMAINDIR"
#define GIT_ATTR_SOURCE_ENVIRONMENT "GIT_ATTR_SOURCE"
#define GIT_ATTACHMENTS_REF_ENVIRONMENT "GIT_ATTACHMENTS_REF"
#define GIT_ATTACHMENTS_DEFAULT_REF "refs/attachments/commits"

/*
* Environment variable used in handshaking the wire protocol.
Expand Down Expand Up @@ -190,6 +192,8 @@ enum object_creation_mode {
OBJECT_CREATION_USES_RENAMES = 1
};

extern char *attachments_ref_name;

extern enum object_creation_mode object_creation_mode;

extern char *notes_ref_name;
Expand Down
18 changes: 18 additions & 0 deletions t/t6700-attach.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh

test_description='basic git attach test cases'

. ./test-lib.sh

test_expect_success 'setup' '
test_commit 1st
'

test_expect_success 'attach: add regular files' '
echo "content of foo" >foo &&
echo "conteng of bar" >bar &&
foo_blob=`cat foo | git hash-object --stdin` &&
bar_blob=`cat bar | git hash-object --stdin` &&
git attach add foo bar
'
test_done

0 comments on commit 8487565

Please sign in to comment.