@@ -3033,6 +3033,155 @@ tree object, generate and store the corresponding commit object, and
3033
3033
update the HEAD branch to the new commit (remember: a branch is just a
3034
3034
ref to a commit).
3035
3035
3036
+ #+begin_src python :tangle libwyag.py
3037
+ argsp = argsubparsers.add_parser("commit", help="Record changes to the repository.")
3038
+
3039
+ argsp.add_argument("-m",
3040
+ metavar="message",
3041
+ dest="message",
3042
+ help="Message to associate with this commit.")
3043
+ #+end_src
3044
+
3045
+ Before we get to the interesting details, we will need to read git's
3046
+ config to get the name of the default user, which we'll use as the
3047
+ author of commits. We'll use the same =configparser= library we've
3048
+ used to read repo's config.
3049
+
3050
+ #+begin_src python :tangle libwyag.py
3051
+ def gitconfig_read():
3052
+ xdg_config_home = os.environ["XDG_CONFIG_HOME"] if "XDG_CONFIG_HOME" in os.environ else "~/.config"
3053
+ configfiles = [
3054
+ os.path.expanduser(os.path.join(xdg_config_home, "git/config")),
3055
+ os.path.expanduser("~/.gitconfig")
3056
+ ]
3057
+
3058
+ config = configparser.ConfigParser()
3059
+ config.read(configfiles)
3060
+ return config
3061
+ #+end_src
3062
+
3063
+ And just a simple function to grab, and format, the user identity:
3064
+
3065
+ #+begin_src python :tangle libwyag.py
3066
+ def gitconfig_user_get(config):
3067
+ if "user" in config:
3068
+ if "name" in config["user"] and "email" in config["user"]:
3069
+ return "{} <{}>".format(config["user"]["name"], config["user"]["email"])
3070
+ return None
3071
+ #+end_src
3072
+
3073
+ Now for the interesting part. We first need to build a tree from the
3074
+ index. This isn't hard, but notice that while the index is flat (it
3075
+ stores full paths for the whole worktree), a tree is a recursive
3076
+ structure. What we do is start from the deepest directory, build a
3077
+ tree with its contents, write that tree to the repository, add a
3078
+ reference to that tree to its parent, and resume at upper levels until
3079
+ we hit home. The /last/ tree we'll have built is the only one we'll
3080
+ need in the commit, since it's the root and references all others ---
3081
+ so this function will simply return its hash.
3082
+
3083
+ #+begin_src python :tangle libwyag.py
3084
+ def tree_from_index(repo, index):
3085
+ contents = dict()
3086
+ contents[""] = list()
3087
+
3088
+ # Enumerate entries, and turn them into a dictionary where keys
3089
+ # are directories, and values are lists of directory contents.
3090
+ for entry in index.entries:
3091
+ dirname = os.path.dirname(entry.name)
3092
+
3093
+ # We create all dictonary entries up to root (""). We need
3094
+ # them *all*, because even if a directory holds no files it
3095
+ # will contain at least a tree.
3096
+ key = dirname
3097
+ while key != "":
3098
+ if not key in contents:
3099
+ contents[key] = list()
3100
+ key = os.path.dirname(key)
3101
+
3102
+ # For now, simply store the entry in the list.
3103
+ contents[dirname].append(entry)
3104
+
3105
+ # Get keys (= directories) and sort them by length, descending.
3106
+ # easiest way to traverse the list of paths bottom-up. This means
3107
+ # that we'll always encounter a given path before its parent,
3108
+ # which is all we need.
3109
+ sorted_paths = sorted(contents.keys(), key=len, reverse=True)
3110
+ sha = None
3111
+
3112
+ # We create, and write, trees
3113
+ for path in sorted_paths:
3114
+ tree = GitTree()
3115
+
3116
+ for entry in contents[path]:
3117
+ if isinstance(entry, GitIndexEntry):
3118
+ # We transcode the mode: the entry stores it as integers,
3119
+ # we need an octal ASCII representation for the tree.
3120
+ leaf_mode = "{:02o}{:04o}".format(entry.mode_type, entry.mode_perms).encode("ascii")
3121
+ leaf = GitTreeLeaf(mode = leaf_mode, path=os.path.basename(entry.name), sha=entry.sha)
3122
+ else:
3123
+ leaf = GitTreeLeaf(mode = b"040000", path=entry[0], sha=entry[1])
3124
+
3125
+ tree.items.append(leaf)
3126
+
3127
+ # Write the new tree object.
3128
+ sha = object_write(tree, repo)
3129
+
3130
+ # Add the new tree to its parent.
3131
+ parent = os.path.dirname(path)
3132
+ base = os.path.basename(path)
3133
+ contents[parent].append((base, sha))
3134
+
3135
+ return sha
3136
+ #+end_src
3137
+
3138
+ The function to create a commit object is simple enough, it just takes a few arguments.
3139
+
3140
+ #+begin_src python :tangle libwyag.py
3141
+ def commit_create(repo, tree, parent, author, timestamp, tz, message):
3142
+ commit = GitCommit()
3143
+ commit.kvlm[b"tree"] = tree.encode("ascii")
3144
+ if parent:
3145
+ commit.kvlm[b"parent"] = parent.encode("utf8")
3146
+
3147
+ author = author + timestamp.strftime(" %s ") + tz
3148
+
3149
+ commit.kvlm[b"author"] = author.encode("utf8")
3150
+ commit.kvlm[b"committer"] = author.encode("utf8")
3151
+ commit.kvlm[b''] = message.encode("utf8")
3152
+
3153
+ return object_write(commit, repo)
3154
+ #+end_src
3155
+
3156
+ And finally, the actual =cmd_commit=, the bridge to the =wyag commit= command:
3157
+
3158
+ #+begin_src python :tangle libwyag.py
3159
+ def cmd_commit(args):
3160
+ print(args)
3161
+ repo = repo_find()
3162
+ index = index_read(repo)
3163
+ # Create trees, grab back SHA for the root tree.
3164
+ tree = tree_from_index(repo, index)
3165
+
3166
+ # Create the commit object itself
3167
+ commit = commit_create(repo,
3168
+ tree,
3169
+ object_find(repo, "HEAD"),
3170
+ gitconfig_user_get(gitconfig_read()),
3171
+ datetime.now(),
3172
+ "+0200", # @FIXME
3173
+ args.message)
3174
+
3175
+ # Update HEAD
3176
+ active_branch = branch_get_active(repo)
3177
+ if active_branch: # If we're on a branch, we update refs/heads/BRANCH
3178
+ with open(repo_file(repo, os.path.join("refs/heads", active_branch)), "w") as fd:
3179
+ fd.write(commit + "\n")
3180
+ else: # Otherwise, we update HEAD itself.
3181
+ with open(repo_file(repo, "HEAD"), "w") as fd:
3182
+ fd.write("\n")
3183
+ #+end_src
3184
+
3036
3185
* Final words
3037
3186
3038
3187
** Comments, feedback and issues
0 commit comments