Skip to content

Commit

Permalink
gitfs, wiki: Add Page Rename feature.
Browse files Browse the repository at this point in the history
This commit also introduces limited Rename Tracking.

Rename Tracking works only under the certain conditions; The content of
the file needs not to be modified at the commit the file is renamed. If
you always edit all pages with only this application, it is no problem
because if you rename and edit a page, the rename and editing will be
commited separately. But if you push commits here from a repository
which is not managed by this application, Rename Tracking may not work
very well as git.
  • Loading branch information
eungjun-yi committed Aug 8, 2012
1 parent 1b64bb1 commit 1512ae7
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 123 deletions.
271 changes: 202 additions & 69 deletions lib/gitfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,13 +399,50 @@ var _parseCommitBody = function(buffer) {
};

/**
* Find the file with the given `blobName` in the `tree`.
* Find the file by the given `targetId` in the `tree`.
*
* @param {Buffer} tree
* @param {String} targetId
* @private
*/
var _findInTreeById = function(tree, targetId) {
var begin, expected, matching, result, pointer, thisId;

begin = null;
matching = false;
result = [];
pointer = 0;
thisId = null;

pointer += 6; // "100644"
pointer += 1; // <SP>
begin = pointer;

for (; pointer < tree.length; pointer++) {
if (tree[pointer] === 0) {
pointer += 1; // <NUL>
thisId = tree.toString('hex', pointer, pointer + 20);
if (targetId === thisId) {
return tree.toString(ENCODING, begin, pointer - 1);
}
pointer += 20; // sha-1
pointer += 6; // "100644"
pointer += 1; // <SP>
begin = pointer;
}
}

return false;
}

/**
* Find the file by the given `blobName` in the `tree`.
*
* @param {Buffer} tree
* @param {String} blobName
* @private
*/
var _findInTree = function(tree, blobName) {
var _findInTreeByName = function(tree, blobName) {
var begin, expected, matching, result, pointer;

begin = null;
Expand All @@ -415,22 +452,15 @@ var _findInTree = function(tree, blobName) {

for (pointer = 0; pointer < tree.length; pointer++) {
if (tree[pointer] === 0) {
if (matching) {
if (tree.toString(ENCODING, begin, pointer) == blobName) {
result = tree.toString('hex', pointer + 1, pointer + 1 + 20);
return result;
}
pointer += 20;
}

if (begin !== null) {
matching = tree[pointer] === expected[pointer - begin];
if (!matching) {
begin = null;
}
}

if (tree[pointer] === 32) {
begin = pointer + 1;
pointer += 1; // <NUL>
pointer += 20; // sha-1
pointer += 6; // "100644"
pointer += 1; // <SP>
begin = pointer;
}
}

Expand Down Expand Up @@ -580,26 +610,68 @@ var _readObjectFromLoose = function(id, callback) {
});
};

var findInTree = function(id, blobName, callback) {
assert.equal(typeof id, 'string');

if (!id) {
return callback(new GitFsError("object id is empty: " + id));
var validateObjectId = function(id) {
if (typeof id !== 'string' || !id) {
throw new GitFsError("object id is empty: " + id);
}

if (id.toString().length > OBJECT_ID_LENGTH) {
return callback(new GitFsError("id.length > " + OBJECT_ID_LENGTH + ": " + id));
throw new GitFsError("id.length > " + OBJECT_ID_LENGTH + ": " + id);
}

return true;
};

/**
* Find the filename by the given `targetId` in the tree matched given 'treeId'.
*
* @param {Buffer} tree
* @param {String} targetId
* @public
*/
var findInTreeById = function(treeId, targetId, callback) {
try {
validateObjectId(treeId);
validateObjectId(targetId);
} catch (e) {
return callback(e);
}

var cb = function(err, type, body) {
if (err) return callback(err);
return callback(null, _findInTree(body, blobName));
return callback(null, _findInTreeById(body, targetId));
};

if (path.existsSync(_getObjectPath(id))) {
return _readObjectFromLoose(id, cb);
if (path.existsSync(_getObjectPath(treeId))) {
return _readObjectFromLoose(treeId, cb);
} else {
return _readObjectFromPack(id, cb);
return _readObjectFromPack(treeId, cb);
}
};

/**
* Find the object id by the given `name` in the tree matched given 'treeId'.
*
* @param {Buffer} treeId
* @param {String} name
* @public
*/
var findInTreeByName = function(treeId, name, callback) {
try {
validateObjectId(treeId);
} catch (e) {
return callback(e);
}

var cb = function(err, type, body) {
if (err) return callback(err);
return callback(null, _findInTreeByName(body, name));
};

if (path.existsSync(_getObjectPath(treeId))) {
return _readObjectFromLoose(treeId, cb);
} else {
return _readObjectFromPack(treeId, cb);
}
};

Expand Down Expand Up @@ -727,7 +799,8 @@ var _computeCommitOffset = function(query, callback) {
*
* Query:
* - `filename` 파일명 e.g.) 'frontpage'
* - `until` 가져올 커밋의 범위 (이 커밋까지) e.g.) 'f96845522d7a2de3c0c4677ac96f6640ceb19055'
* - `blobId` (optional) blob id e.g.) 'f2ba8f84ab5c1bce84a7b441cb1959cfc7093b7f'
* - `until` (optional) 가져올 커밋의 범위 (이 커밋까지) e.g.) 'f96845522d7a2de3c0c4677ac96f6640ceb19055'
* - `offset` until에 대한 offset (음수도 가능) e.g.) 1
* - `since` 가져올 커밋의 범위 (이 커밋 이후) e.g.) '0cc71c0002496eccbe919c2e5f4c0616f9f2e611'
* - `limit` 가져올 커밋의 최대 갯수. e.g.) 30
Expand Down Expand Up @@ -770,8 +843,6 @@ var queryLog = function(query, callback) {
var _queryLog = function(query, callback) {
// This function does not consider query.offset. It should be processed
// before calling this.
var commit, empty;

assert.ok(typeof callback === 'function');

// Return an empty array if there is no need to find logs.
Expand All @@ -792,7 +863,9 @@ var _queryLog = function(query, callback) {
filename: query.filename,
until: commit.parent,
since: query.since,
limit: newLimit
limit: newLimit,
blobId: query.blobId,
lastChildLostFile: query.lastChildLostFile
}, function(err, nextCommits) {
if (err) return callback(err);

Expand All @@ -807,61 +880,113 @@ var _queryLog = function(query, callback) {
});
};

var __getBlobId = function(treeId, cb) {

// If commit.tree is memoized, get it using readObject.
// Else determine whether commit.tree has a blob matched
// query.filename using findInTree which is much faster than
// readObject.
var __findBlobInTree = function(treeId, cb) {
if (memo[treeId]) {
// If commit.tree is memoized, get it using readObject.
readObject(treeId, function(err, tree) {
var matchedId = tree[query.filename];

if (err) return callback(err);
return cb(err, tree[query.filename]);

if (matchedId) {
return cb(err, matchedId);
} else if (query.blobId) {
for (var fn in tree) {
if (tree[fn] == query.blobId) {
return cb(err, null, fn);
}
}
}

return cb(err);
});
} else {
findInTree(treeId, query.filename, function(err, blobId) {
return cb(err, blobId);
// Else determine whether commit.tree has a blob matched
// filename or blobId using findInTree* which is much faster than
// readObject.
findInTreeByName(treeId, query.filename, function(err, matchedId) {
if (matchedId) {
return cb(err, matchedId);
} else if (query.blobId) {
return findInTreeById(treeId, query.blobId, function(_err, matchedName) {
if (matchedName) {
return cb(_err, null, matchedName);
} else {
return cb(err);
}
});
}

return cb(err);
});
}

};

return __getBlobId(commit.tree, function(err, blobId) {
return __findBlobInTree(commit.tree, function(err, matchedId, matchedName) {
if (err) return callback(err);

var commits = null;
var commits = [];
commits.ids = [];

// In this tree, there is no file whose name is equal to query.filename.
// It means the file was deleted or renamed.
if (!matchedId) {

// Existence of matchedName means the file was renamed.
if (matchedName) {
// Add an entry about the rename, into the commit log.
query.filename = matchedName
commit.filename = query.filename
commits = [commit];
commits.ids = [query.until];
return goNext(commits, query.limit - 1, callback);
}

// The file was deleted or not created yet. Just keep this commit
// because I'm not sure whether the file was deleted.
if (query.blobId) {
query.lastChildLostFile = commit;
query.lastChildLostFile.filename = query.filename;
query.lastChildLostFile.id = query.until;
return goNext(commits, query.limit - 1, callback);
}

// FIXME: The if-clause below ignores a commit whose tree has
// no blob matched query.filename. But for rename tracking and
// deletion detecting, it shouldn't do that.
if (!blobId) {
// Skip this commit.
return goNext(commits, query.limit, callback);
}

if (query.lastChildLostFile) {
// At last child commit, the file was deleted.
// Add an entry about the deletion.
commits = [query.lastChildLostFile];
commits.ids = [query.lastChildLostFile.id];
delete query.lastChildLostFile;
}

// If the current commit has a blob matched query.filename
// but no parent. it means that the current commit is the root
// commit and so the first commit at which the blob is created.
if (!commit.parent) {
commits = [commit];
commits.ids = [query.until];
commits.push(commit);
commits.ids.push(query.until);
return goNext(commits, query.limit - 1, callback);
}

// Read the parent of the current commit and add it into commits
// if its blob matched query.filename is not identical with
// the current commit's blob matched query.filename.
return readObject(commit.parent, function(err, parentCommit) {
if (err) return callback(err);
// If matchedId differs with query.blobId, it means the contents of
// the file was changed.
if (matchedId != query.blobId) {
// Add an entry into commit log about the changes.
commit.filename = query.filename
commits.push(commit);
commits.ids.push(query.until);
query.blobId = matchedId;
return goNext(commits, query.limit - 1, callback);
} else {
// Nothing was changed. Skip this commit.
return goNext(commits, query.limit, callback);
}

return __getBlobId(parentCommit.tree, function(err, nextBlobId) {
if (blobId != nextBlobId) {
commits = [commit];
commits.ids = [query.until];
return goNext(commits, query.limit - 1, callback);
} else {
return goNext(commits, query.limit, callback);
}
});
});
});

});
Expand Down Expand Up @@ -1050,27 +1175,35 @@ var _diff = function(name, a, b, type, callback) {
* @return {String|Object}
* @api public
*/
var diff = function(name, a, b, types_, callback) {
var diff = function(nameA, nameB, revA, revB, types_, callback) {
var types = (types_ instanceof Array) ? types_ : [types_];
var results = {};

async.forEach(types, function(type, cb) {
switch (type) {
case 'bool':
return _getBlobId(name, a, function(err, blobA) {
_getBlobId(name, b, function(err, blobB) {
return _getBlobId(nameA, revA, function(err, blobA) {
_getBlobId(nameB, revB, function(err, blobB) {
results[type] = (blobA == blobB);
return cb();
});
});
case 'unified':
case 'json':
default:
return show(name, a, function(err, contentA) {
show(name, b, function(err, contentB) {
var A = contentA.toString();
var B = contentB.toString();
_diff(name, A, B, type, function(err, diff) {
return show(nameA, revA, function(err, contentA) {
show(nameB, revB, function(err, contentB) {
var A = '', B = '';

if (contentA) {
A = contentA.toString();
}

if (contentB) {
B = contentB.toString();
}

_diff(nameA, A, B, type, function(err, diff) {
results[type] = diff;
return cb();
});
Expand Down
Loading

0 comments on commit 1512ae7

Please sign in to comment.