2017总结之GIT

从去年下半年开始就一直在使用 SourceTree 一些常用功能在图形化的界面里显的更直观。但是在处理一些复杂操作时似乎对 git 的命令了解的越透彻越能快速的解决问题。所以还是把快要生疏的命令拿出来理理吧。

基本原理

大多数命令都是在这三者之间工作。
其中仓库是一个个提交的节点前后串联,并且有各种指针来表示不同的演进历史,也就是分支。不同的文件状态是由索引与工作目录的差异造成的。

  • 工作目录 (Work Directory)
  • 缓存区 (Index)
  • 仓库(History)

看这样的图解GIT会理解的更好。

一些基本命令

git checkout dev

从当前分支切换到 dev

git checkout -b featrue/foo

创建一个新的分支,并切换到这个分支里

git checkout –track origin/bar

在本地创建一个 origin/bar 的跟踪分支

git add .

添加当前目录下所有文件到 index

git add foo.js

添加 foo.js 到 index

git commit -m ‘message’

提交一个 commit 到当前分支中

git commit -am ‘message’

提交一个 commit 对象到当前分支,并且把目录中没有 add 的跟踪对象 tracked file 一起提交。注意:不包括未跟踪的对象 –all

git commit –amend

运行一次 add 动作并把这次提交的文件及信息添加到上一次的 commit 对象中,其实就是丢弃上一次的提交

git push origin master

把当前分支推送到 originmaster 分支

git push origin :featrue/foo

注意 : 前是有一个空格的,这样的提交会删除远程分支

git log

查看当前分支的日志情况

git log -2

查看当前分支日志的前两条

git log -p -1

查看当前分支日志的第一条,及它的修改内容

git log -p –graph – file.js

图形化的方式显示日志

git branch -d [branch name]

删除一个本地分支

git push origin [注意这里有一个空格]:[remote branch name]

删除服务端分支

git branch

查看所有仓库所有分支

git branch -vv

查看本地分支的更多详情,了解本地分与远端的关系

git stash save

把当前的未跟踪修改保存起来

git stash pop

把保存的修改恢复到当前分支

git merge origin foo

foo 分支合并到当前分支中

git merge origin foo –ff-only

只能以快进 fast-forward 的方式合并,分叉了就不行了。
其它还有 --ff --no-ff

高级 - 撤销

git checkout – foo.js

从缓存区恢复这个文件

git checkout HEAD – foo.js

从 HEAD 指向的历史记录中恢复这个文件,并且把 缓存区一起恢复

git reset – foo.js

同上

git reset –hard HEAD

从 HEAD 把工作区加缓存区都一起恢复

git reset –soft HEAD~

只是让本地的 History 回到上一次 commit 状态

git reset HEAD~

让本地 History 回到上一条 commit 状态 并且把 Index 恢复,但不管 Working Directory

高级 - 恢复

git log -p – [filename]

查找已经删除的文件

git log -g

当分支意外删除时,可以加 ‘-g’ 找到最后一次提交的 commitID,然后从这个commitID 恢复一个新的分支出来

git reflog

可以查看 HEAD 指针变化的历史,方便做一个恢复的工作。

Git Hook 的小实践

先贴上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env ruby
# encoding: UTF-8

$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]


$test_domain_regex = /\.test\.51offer\.com/

def check_test_domain_in_template

change_files = `git rev-list --objects #{$oldrev}..#{$newrev} | git cat-file --batch-check='%(rest)' | egrep '\.(jsp|vm|html)$'`.split("\n")

change_files.each do |path|

file_content = `git show #{$newrev}:#{path}`

if $test_domain_regex.match(file_content)

puts "[POLICY] 不允许在 vm、jsp、html 等文件中包含 'test.51offer.com' 这样的字符 > #{path}"
exit 1

end
end
end

check_test_domain_in_template

需求背景

我们发生过几次线上事故,就是带有 .test.51offer.com 的内容发布到正式环境,在内网很难主动发现这样的问题,所以需要对提交的代码做一些检查,自然的想到了 Git Hook

什么是 Git Hook

Hook 指的是钩子,这里可以理解成某种事件所触发的程序。比如:你在本地做 commit 或者 pull 时都可以触发 Git 中相应的 Hook 脚本。Hook 脚本又分客户端和服务端。它们响应不同的事件。 Hook脚本可以使用 Ruby, Python 和 Shell 来编写。因为分发的问题,解决上面的需求我选择了服务端 Hook,并把代码写在 update 事件中,希望仓库的分支在被更新时触发检查。

Git Hook 的详情介绍可以查看下面的地址:
https://git-scm.com/book/zh/v1/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git%E6%8C%82%E9%92%A9

技术关键点

最关键的是要在客户端在一次 push 中所有 commit 对象中找到你要的文件。连 Git 基本命令还在熟悉的我只能请教 stackoverflow.com 了。原文地址:http://stackoverflow.com/questions/1595631/how-to-get-a-list-of-all-blobs-in-a-repository-in-git

代码如下:

1
$ git rev-list --objects --all | git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' | grep '^[^ ]* blob' | cut -d" " -f1,3-

简化成我需要的:

1
git rev-list --objects #{$oldrev}..#{$newrev} | git cat-file --batch-check='%(rest)' | egrep '\.(jsp|vm|html)$'

使用了 rev-listcat-file 两个 git 的底层命令,含义就是找出两个 commit id 之间所有类型的提交对象(commit,tree,blob),并一一查看他们,显示出他们对应的文件路径(除 blod 对象,其它类型是没有路径的),然后只拿带有那几项扩展名的路径。

有了这些路径,我只需要使用 git show 命令从最新版本中捞出这些文件内容就可。

这里一个细节:grep 需要加 -E 才能支持正则表达式,否则是使用通配符,egrep 是直接使用正则表达式来做匹配的。

如何部署

因为我们使用了 Git Lab ,我从官方上找来了安装指南:

http://doc.gitlab.com/ce/hooks/custom_hooks.html

setup 比较简单,创建后如下图:

一个 git 仓库目录中的文件

需要给 custom_hooks 目录及文件给于 git 用户执行权限:

1
2
3
4
5
mkdir custom_hooks
chmod 777 custom_hooks
## 上传 update 文件
chmod 775 custom/update
chown git.git custom_hooks/ -R

在本地提交验证 hook 是否工作

执行 push 检查 Hook 有效性

以上方案还是只在一个仓库中部署,需要所有仓库布置这样的 Hook 如何处理?

第一图上已经给出答案:/opt/gitlab/embedded/service/gitlab-shell/hooks 这里是 Gitlab 自带的一些 hook 如下图:

查看gitlab hook

显然使用 require_relative 就可以把我们要加入的内容塞进去。

至此,我们用 hook 处理了这样的需求。暂时只部署在下面的仓库中,大家可以玩一下: git@gitlab.51offer.inner:libo/pack-test.git

后记

整理了一个比较适合在 gitlib 中使用的版本,这里再次遇到一个编码问题,为了统一源码编码格式和读取外部文件的编码格式引用了 nkf 模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env ruby
# encoding: UTF-8

require 'nkf'

class Git51offerCustomHook

def checkTestDomainOnUpate(oldrev, newrev)

testDomainRegex = /\.test\.51offer\.com/

changeFiles = `git rev-list --objects #{oldrev}..#{newrev} | git cat-file --batch-check='%(rest)' | egrep '\.(jsp|vm)$'`.split("\n")

perfect = true

changeFiles.each do |path|

fileContent = `git show #{newrev}:#{path}`
fileContent = NKF.nkf("-w", fileContent)

if testDomainRegex.match(fileContent)
puts "[POLICY] 不允许在 vm、jsp 等文件中包含 'test.51offer.com' 这样的字符 > #{path}"
perfect = false
break
end

end

return perfect

end

end

注意

update hook 只能对已经存在的文件进行处理,新提交的文件不会触发

update

新版的gitlab已经使用自定义 update.d 文件来支持自定义hook了,这样的好处是,即使更新 gitlab 你的自定义文件还是存在的,不需要手动从系统全局的 hook 文件中添加

django + django-cms 开发实战-django-cms插件开发

django的设计处处体现了高内聚,低耦合的设计思想。比如APP的设计,可轻松从一个project移植到另一个project中。django-cms内部各种功能也是插件的方式提供。
初次安装的django-cms,可能只有一个现呈的page模块,如果你需要更高级的功能还是同样需要设计APP。django-cms设计了一些方法可以很方便让我们自己的APP与Page一起工作。

编写插件

我们已经有了之前设计的Products APP,他有自己的模板,有自己访问的URL,但如果我想在首页显示我置顶的产品,那就需要制作一个django-cms的插件了。

django-cms的插件中把需要记录的参数(比如我要显示哪些分类共多少条置顶的产品)做为model, 在django-cms为placeholder添加你的插件时,这个model中的项会出现在界面上供你修改:

from cms.models import CMSPlugin
class ProductPlugin(CMSPlugin):
    # 使用多对多关系admin中对应多选控件
    cates = models.ManyToManyField(Categroy, related_name='plugins')
    number = models.IntegerField(default = 6)

    def __unicode__(self):
      # 对于多对多的字段内部读取时可以用.all()来访问
      return "%s [%s]" % ("|".join([t.title for t in self.cates.all()]), self.number)

有了这个model然后创建真正的插件文件cms_plugins.py:

from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from products.models import ProductsPlugin as ProductsPluginModel
from products.models import Product, Categroy
from django.utils.translation import ugettext as _

class ProductsPlugin(CMSPluginBase):
    model = ProductsPluginModel
    name = _("Products Top Plugin")
    render_template = "products/plugin_top.html"

    def render(self, context, instance, placeholder):
        pros = Product.objects.filter(categroy__in = instance.cates.all()).filter(is_top = True)[:instance.number]
        context.update({'instance':instance, "products" : pros})
        # context会传递到 render_template定义的模版中
        return context

# 注册到django-cms的插件池中
plugin_pool.register_plugin(ProductsPlugin)

此时django-cms的page栏目里,你就可以为placeholder中添加名为’Products Top Plugin’的自定义插件了。

编写应用钩子

之前在project中的url里添加了一条正则把路径串联到APP中:

url(r'^products/', include("products.urls")),

如果需要django-cms中任意page自定义的页面进入我们的APP如何做到呢? 那需在制作一个app的钩子。

from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
from django.utils.translation import ugettext_lazy as _

class ProductsApp(CMSApp):
    name = _("Products App")
    urls = ["products.urls"]

apphook_pool.register(ProductsApp)

有了这个,就可以把project中urls.py里的上述那条url正则去掉了。在创建的page的高级选项中,为”Application”选择我们创建的 “Products App”,那么访问这个page 时会自动进入products的url匹配。

菜单

django-cms中的菜单是可以自己定义的,你可以把任一page页面加入到菜单中,然后通过 ‘show_menu’ 标签调用。那我们自己编写的APP如何加入到菜单中呢。特别时APP也有子菜单的话,如何也显示这些子菜单?那需要 menu.py 出场了。

from cms.menu_bases import CMSAttachMenu
from menus.base import Menu, NavigationNode
from menus.menu_pool import menu_pool
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from products.models import Categroy

class ProductsMenu(CMSAttachMenu):
    name = _("Product Menu")
    def get_nodes(self, request):
        nodes = []
        for cate in Categroy.objects.all():
            node = NavigationNode(
                cate.title,
                reverse('products.views.categroy', args=(cate.path,)),
                cate.id,
                cate.parent and cate.parent.id or None # 有没有父节点
            )
            nodes.append(node)
        return nodes

menu_pool.register_menu(ProductsMenu)

ProductsMenu还需要添加到 cms_app.py中:

class ProductsApp(CMSApp):
    name = _("Products App")
    urls = ["products.urls"]
    # 添加到这里
    menu = ["products.menu"]

接下来的工作是告诉django这个菜单显示在哪个page下面。可以在page的高级设置中的 “Attached menu” 进行选择,最后就剩下在模板中调用了 ‘load menu_tags’并使用标签 ‘show_menu 0 100 100 100’ 显示所有菜单项。

show_menu的参数具体说明可参看: https://django-cms.readthedocs.org/en/2.3.5/getting_started/navigation.html#show-menu

好了,以上就是这次django-cms学习的一些记录,大致上可以应付自己的简单要求了,整个过程感觉django的设计非常巧妙,很多地方为程序员节省了很多工作。django-cms也充满DIY精神,为千变万化的定制需求提供了条件。

参考:

django + django-cms 开发实战-APP开发

上一篇做了基础工作,完成了django与django-cms的安装与配置,繁琐的是它们那些依赖也要一项一项的去了解,不会有一键安装这种好事。今天这一篇将创建我的第一个APP。他是一个简单的产品管理与展示系统。let’s go!

模型

python manage.py startapp products

在已有的Project中使用这个命令可以自动创建一个默认的APP(一个python模块)。他包括四个默认文件。我的APP需要数据库支持,所以首先要在models.py中设计数据库结构。django提供了非常方便的数据库对象化方案。具体的类如下:

import os
from datetime import date
from django.db import models
from filebrowser.fields import FileBrowseField

# 每个类代表一张表,类的属性指代表的字段
class Categroy(models.Model):
    # name用于后台显示,title用于前端页面显示
    name = models.CharField(max_length = 100)
    title = models.CharField(max_length = 100)
    order = models.IntegerField(default = 0)
    # 用户可以自定义路径,及此分类下的模板
    path = models.CharField(max_length = 20, blank=False)
    list_template = models.CharField(max_length = 200, blank = False, default = "products/list.html")
    item_template = models.CharField(max_length = 200, blank = False, default = "products/item.html")
    keywords = models.CharField(max_length = 300, blank = False)
    descoription  = models.TextField(blank = False)
    display = models.BooleanField(default = True)
    add_time = models.DateTimeField(auto_now_add = True)
    # 支持无限分类, 注意与自己做一对一关系时,第一个参数使用字符型的self,不能直接用变量,因为它指代的是实例
    parent = models.ForeignKey('self', blank = True, null = True)
    # 使用一个递归来计算当前分类的层级
    def _count_level(self):
        def loop_count_level(parent, level = 2):
            if parent.parent:
                return loop_count_level(parent.parent, level + 1)
            else:
                return level
        if self.parent == None:
            return 1
        else:
            return loop_count_level(self.parent)
    # 可以自定义一些方法做一些计算,同时用property把它变成实例的属性
    level = property(_count_level)
    def _children(self):
        # 使用 ‘__’ 双下划线可以指代关联表中的字段
        return Categroy.objects.filter(parent__id = self.id)
    children = property(_children)
    def _products(self):
        return Product.objects.filter(categroy = self)
    products = property(_products)
    def _all_products(self):
        cates = [self] + [c for c in self.children]
        # 使用 ‘__’ 双下划线还可以做一些简单的字段查询
        return Product.objects.filter(categroy__in = cates)
    all_products = property(_all_products)
    # 在没有明确指明时,实例可以返回的字符串信息
    def __unicode__(self):
        return "%s %s" % ("-" * self.level, self.name)

class Product(models.Model):
    ....

安装APP

创建完表字段后,我们有两步要做,一是把APP安装到django中:

INSTALLED_APPS = (
    ...
    'products',
    ...
)

二是要使用命令在数据库中生成真正的表:

python manage.py syncdb --all

在admin中管理

django的admin后台为程序员节省了很多时间,它可以快速为你自己的APP搭建一个后台。我们需要在APP中创建一个admin.py文件,在其中把我们的数据模型注册到admin中,代码如下:

from django.contrib import admin
from news.models import Categroy, Products

admin.site.register(Categroy)
admin.site.register(Products)

再次登录后台会发现已经可以看到Categroy和Products的管理条目了,基本的删查改都提供。这时还有个问题,文本编辑器(tinyMCE)没有出现,接下来要对admin.py做一些定制工作:

# 继承ModelForm类可以控制admin后台中编辑form中的项
class ProductForm(forms.ModelForm):
    content = forms.CharField(widget=TinyMCE(attrs={'cols': '90', 'rows': '60'}))
    class Meta:
        model = Product

class ProductAdmin(admin.ModelAdmin):
    form = ProductForm

admin.site.register(Categroy)
admin.site.register(Product, ProductAdmin)

相当于覆盖了默认的表单内容。django还提供了很多功能让用户可以方便的定制后台。

页面展示

页面展示需要考虑两个问题:通过什么样的URL访问;如何展示内容。django通过 urls.py 来设置访问的url,views.py 则包括所有的视图代码。
其实我们project创建时已经有了一个urls.py,把需要访问的url都设置在此处也没有问题,但为了减少耦合,尽量把app自己的url规则配置在自己的urls.py中。只需要在project中的urls.py中设置如下代码,即可把访问串联起来:

url(r'^products/', include("product.urls")) # 注意正则中并没有出现"$"结束符

每一个url正则对应一个view, 可以多个url正则对应一个view。没有命名的正则表达式组,按顺序对应view的参数,而命名的正则表达式组对应view参数的名称。

(r'^(?P<id>[0-9]+)/?$', views.product) # product方法中的参数id 可以获取url中匹配到的id

views中用作返回视图的方法,总能接收一个’request’参数,而它本身必须返回一个 HttpResponse对象

def product(request, id=""):
    try:
        id = int(id)
        product = Product.objects.get(id = id)
    except Product.DoesNotExist:
        return Http404
    # render_to_response能快速的返回HttpResponse对象
    return render_to_response(product.categroy.item_template,
                              {"product":product}, context_instance=RequestContext(request)) 
    # context_instance 上下文实例可以让你在模板中访问一些常规的变量,
    # 比如,当前用户这样的信息,不需要你自己往 dictionary 加入这些内容。

模板

模板的内容相对简单,前端对这些比较熟悉。

django + django-cms 开发实战-配置

因为从window主机环境换到VPS,把手上的站点都移到linux主机下,这时需要一个CMS来管理手上的这些老站点。首先想到了django。于是有了这篇实战记录。

需求

  • 需要支持单页面管理:django-cms的page很强大,基本没有问题;
  • 支持产品添加管理,显示产品列表及单品;
  • 产品支持搜索等功能;
  • 有图片及文件管理功能。

看来只需要做一个简单的产品管理就可以了。

安装

django与django+cms,及所需要的APP的安装:

  • django 1.4
  • django-cms 2.3.5
  • tinymce 1.5.1b4
  • django-filebrowser-no-grappelli-django14
  • easy-thumbnails 1.2

每个项目本身也会依赖一些其它组件,项目主页上基本会有所提示。通过 pip 安装会比较顺利:

sudo apt-get install pip

这里要说一下,对于新手,我会觉得django有一个好主题grappelli, 有一个好的文件管理组件filebrowser, django-cms更是用户众多,tinymce也像是默认搭配的编辑器。但可惜他们如果放到一起工作就不会太顺利了,只能放弃一些选择。所以你这里会看到 django-filebrowser-no-grappelli-django14 这个玩意。其实django-filer也不错,但tinymce内部似乎只支持了filebrowser。

创建Project

django提供了方便的创建项目的工具:

django-admin startproject myCMS

创建好的项目会有一些基础配置文件,及一个工具文件,后续还会用到它。

settings配置

最重要的配置文件就是settings.py。settings中有以下项目需要理解:

DATABASES

用来定义django的数据库接口,这里使用方便的sqlite,只需要提供一个文件地址就可以了;
可以增加项目目录的自检查:

import os
PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))

MEDIA_ROOT,MEDIA_URL, STATIC_ROOT, STATIC_URL

依次为文件上传的目录与访问它们的url, project或app所使用的css,js等静态文件的目录及访问他们的url地址。对于URL可以启一个apache服务专门来处理这些文件的请求,比如 http://127.0.0.1:8080 指到 MEDIA_ROOT 指定的目录。

TEMPLATE_CONTEXT_PROCESSORS

此项中定义了很多在模板中可以很方便使用的上下文对象,比如最常用的用户信息,及请求对象(rquest)的信息。

MIDDLEWARE_CLASSES

django的中间件列表。在web请求到达url匹配之前,都会按列表顺序在中间列中被过滤,所以希望全局对请求做一些处理可以开发一个中间件添加到这里。安装django-cms时,有几个cms依赖的中间件必须要添加:

'cms.middleware.page.CurrentPageMiddleware',
'cms.middleware.user.CurrentUserMiddleware',
'cms.middleware.toolbar.ToolbarMiddleware',

TEMPLATE_LOADERS, TEMPLATE_DIRS

模板是如何加载的,以上两项来解决这个问题。loaders定义了一些模板文件导入器,比如filesystem.Loader就是从TEMPLATE_DIRS中定义的目录来加载,而app_directories.Loader则是从app目录下的templates来加载,这里需要注意他们顺序,如果同名的话,就会有使用最先找到的那个模板。为了有所区分,最好的办法就是在templates下面使用project或app的名称做下一级目录:tempates/app/template_a.html。这样就不会有问题了。

INSTALLED_APPS

django的大部分功能是通过APP来实现的,通过此项来组装这些APP。系统会提供很多有用的APP, 我们自己也可以为一些功能开发一个独立的APP添加到这个元组中。以下为后续要用到的app列表:

'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'django.contrib.staticfiles',
'cms',
'menus',
'mptt',
'south',
'cms.plugins.text',
'cms.plugins.picture',
'cms.plugins.link',
'cms.plugins.file',
'cms.plugins.snippet',
'cms.plugins.googlemap',
'sekizai',

'tinymce',
'filebrowser',

'easy_thumbnails'

django-cms安装时也会提供一个settings的模板文件,主要是列出需要的app, 其它设置可以查看文档

其它

一些APP需要的设置

CMS_TEMPLATES = (
    ('example.html', 'Example Template'),
)

CMS_SEO_FIELDS = True
CMS_REDIRECTS = True
CMS_USE_TINYMCE = True

TINYMCE_DEFAULT_CONFIG = {'theme': "advanced", 'relative_urls': False}
TINYMCE_FILEBROWSER = True

FILEBROWSER_DEBUG = True
FILEBROWSER_MEDIA_URL = 'http://127.0.0.1:8081/uploads/'   # 可以在本地启一个apache来访问这些上传文件

URLS配置

如果app在view层有展示,那就需要配置访问他的URL,比如tingMCE编辑器的安装就包括在project中的urls中增加以下代码:

url(r'^tinymce/', include('tinymce.urls'))

运行

要让django运行起来,还是需要执行一些命令,这时需要用到项目创建时自动生成的 ‘manage.py’ 文件。

  • python manage.py syncdb –all生成数据库表
  • python manage.py runserver 让 django运行在自己的web服务器上

现在可以通过 http://localhost:8000/admin 来访问django的admin后台了。

附:

  • 如果发现filebrowser打不开,那很可能是没有在media目录下创建uploads目录,它不是自动创建的。

关于IE6中a链接会对其上的绑定事件造成请求中断的问题

这个是一个不太注意的坑,可能会埋的很深,不经意间害死你!

IE6中在a链接中绑定事件(包括通过ID名那样的绑定),如下:

<a href="javascript:void(0);" onclick="do()">link</a>

当do函数执行ajax加载,或者动态在head区域中增加css, js标签加载资源时, 会造成这些请求的中断问题。

解决的方法是要么取消a的默认事件(e.preventDefault()), 要么不要使用a标签。
解决方法很简单,只是出现问题时,你怎么会想到这里。